Chào bạn,
Ở bài học trước, chúng ta đã tìm hiểu cách thiết kế các quy trình có thể tái sử dụng bằng cách sử dụng Embedded Sub-processes để tổ chức và Call Activities để module hóa. Việc chia nhỏ các quy trình phức tạp thành các module độc lập, dễ quản lý không chỉ giúp sơ đồ rõ ràng hơn mà còn đặt nền tảng quan trọng cho một khía cạnh không thể thiếu trong phát triển phần mềm: kiểm thử (testing).
Trong bài học hôm nay, chúng ta sẽ đi sâu vào kỹ năng này. Mục tiêu của bài học là implement unit testing và integration testing cho các quy trình BPMN. Với kinh nghiệm của một software developer, bạn sẽ thấy rằng các nguyên tắc kiểm thử bạn đã biết như unit test, integration test, và mocking đều được áp dụng một cách tự nhiên trong thế giới Camunda, nhưng với một số công cụ và kỹ thuật đặc thù. Việc viết test tự động sẽ đảm bảo các quy trình của bạn hoạt động đúng như mong đợi, dễ dàng bảo trì và giảm thiểu lỗi khi có sự thay đổi.
1. Các Cấp Độ Kiểm Thử trong Camunda
Trước khi đi vào code, điều quan trọng là phải phân biệt rõ các cấp độ kiểm thử khác nhau trong một dự án Camunda. Tương tự như phát triển phần mềm truyền thống, chúng ta có thể chia thành ba phạm vi chính.
Tài liệu của Camunda (dù là cho phiên bản 8) đã định nghĩa rất rõ ràng về các phạm vi kiểm thử này. Các khái niệm này hoàn toàn có thể áp dụng cho Camunda 7 mà chúng ta đang sử dụng.
Hãy đọc phần đầu tiên có tiêu đề 'Testing scopes'. Tập trung vào định nghĩa của Unit tests, Process tests, và Integration tests.
Dựa vào tài liệu trên, chúng ta có thể tóm tắt như sau:

- Unit Tests: Tập trung vào việc kiểm thử một đơn vị code nhỏ nhất một cách độc lập. Trong Camunda, đây thường là một lớp
JavaDelegatehoặc một biểu thức (expression). Mục tiêu là xác minh logic bên trong lớp đó hoạt động chính xác, hoàn toàn tách biệt khỏi process engine. - Process Tests (Kiểm thử quy trình): Đây là loại kiểm thử quan trọng nhất đối với BPMN. Mục tiêu là kiểm thử luồng logic của mô hình quy trình. Bạn sẽ khởi chạy một thực thể quy trình (process instance) trong một môi trường test (thường là in-memory), sau đó kiểm tra xem nó có đi đúng đường, dừng ở đúng các trạng thái chờ (wait states), và các biến (variables) có được xử lý chính xác hay không. Các phụ thuộc bên ngoài như database hay REST API thường được mock (giả lập).
- Integration Tests (Kiểm thử tích hợp): Kiểm thử toàn bộ hệ thống trong một môi trường gần giống với production nhất có thể. Nó bao gồm cả việc tương tác với các hệ thống bên ngoài thực sự (hoặc các bản mock chất lượng cao như WireMock).
Trong bài học này, chúng ta sẽ tập trung vào Unit Tests và Process Tests, vì đây là hai loại test mà developer như bạn sẽ viết và chạy thường xuyên nhất trong chu trình phát triển.
2. Unit Testing một JavaDelegate
Hãy bắt đầu với phạm vi hẹp nhất: kiểm thử một JavaDelegate. Giả sử chúng ta có một Service Task thực thi logic để kiểm tra tính hợp lệ của đơn hàng.
// Giả định lớp này nằm trong project của bạn
@Component("orderValidator")
public class OrderValidationDelegate implements JavaDelegate {
private final PricingService pricingService;
// Inject service thông qua constructor
public OrderValidationDelegate(PricingService pricingService) {
this.pricingService = pricingService;
}
@Override
public void execute(DelegateExecution execution) throws Exception {
Integer amount = (Integer) execution.getVariable("amount");
String customerId = (String) execution.getVariable("customerId");
boolean isValid = amount > 0 && customerId != null;
boolean isPremium = pricingService.isPremiumCustomer(customerId);
if (isPremium) {
// Giảm giá 10% cho khách hàng premium
amount = (int) (amount * 0.9);
execution.setVariable("amount", amount);
}
execution.setVariable("orderValid", isValid);
}
}
Để unit test lớp OrderValidationDelegate, chúng ta không cần khởi động toàn bộ Camunda engine. Chúng ta chỉ cần:
- Tạo một đối tượng
DelegateExecutiongiả. - Thiết lập các biến đầu vào cho nó (
amount,customerId). - Mock
PricingServiceđể kiểm soát hành vi của nó. - Gọi phương thức
execute(). - Kiểm tra xem các biến đầu ra (
orderValid,amount) có được thiết lập đúng như mong đợi không.
Thư viện camunda-bpm-mockito cung cấp các công cụ tiện lợi để làm việc này.
JUnit5, Mockito, TDD with Camunda BPMN2.0 spring-boot
Bài viết này trên Medium cung cấp một ví dụ tốt về cách sử dụng Camunda Mockito để thực hiện unit test cho một delegate. Hãy xem qua để hiểu cách thức hoạt động.
Hãy đọc các phần 'What is Camunda Mockito?' và 'JUnit5, Mockito Testing for springboot embedded Camunda Engine'. Chú ý đến việc sử dụng DelegateExecutionFake để tạo một đối tượng execution giả cho việc test.
Dưới đây là một ví dụ về test class sử dụng JUnit 5 và Mockito:
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.extension.mockito.delegate.DelegateExecutionFake;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class OrderValidationDelegateTest {
@Mock
private PricingService pricingService;
@Test
void shouldSetOrderAsValidAndApplyDiscountForPremiumCustomer() throws Exception {
// 1. Arrange
OrderValidationDelegate delegate = new OrderValidationDelegate(pricingService);
DelegateExecution execution = new DelegateExecutionFake();
execution.setVariable("amount", 1000);
execution.setVariable("customerId", "PREMIUM_001");
// Giả lập pricingService trả về true cho khách hàng này
when(pricingService.isPremiumCustomer("PREMIUM_001")).thenReturn(true);
// 2. Act
delegate.execute(execution);
// 3. Assert
// Kiểm tra biến đầu ra
assertThat(execution.getVariable("orderValid")).isEqualTo(true);
assertThat(execution.getVariable("amount")).isEqualTo(900);
// Kiểm tra service đã được gọi
verify(pricingService, times(1)).isPremiumCustomer("PREMIUM_001");
}
}
Cách tiếp cận này rất quen thuộc với các developer, giúp bạn kiểm tra logic nghiệp vụ của mình một cách nhanh chóng và hiệu quả.
3. Process Testing: Kiểm thử Toàn bộ Luồng
Đây là phần thú vị nhất: kiểm thử một file .bpmn từ đầu đến cuối. Chúng ta sẽ sử dụng thư viện camunda-bpm-assert để viết các câu lệnh kiểm tra (assertion) một cách trôi chảy và dễ đọc.
Hãy xem một video hướng dẫn thực tế về cách thiết lập và viết một bài process test đơn giản.
How to write test case for camunda | springboot | Setup | Demo
Video này của Dpoint sẽ hướng dẫn bạn từng bước cách viết một bài test cho quy trình Camunda sử dụng Spring Boot. Nó bao gồm từ việc cấu hình dependencies đến việc viết các assertion cho luồng quy trình.
Hãy xem kỹ từ 07:11 đến cuối video. Video này sử dụng JUnit 4 với @Rule, nhưng chúng ta sẽ điều chỉnh một chút cho phù hợp với JUnit 5 và Spring Boot. Hãy tập trung vào các điểm chính sau: Dependencies (07:11): Các thư viện cần thêm vào pom.xml, đặc biệt là camunda-bpm-process-test-coverage và các thư viện assert. Setup và Start Process (08:36): Cách khởi tạo process engine và bắt đầu một process instance bằng process key. Assertions (12:28): Cách sử dụng các câu lệnh assert như isActive(), hasPassed(), isWaitingAt(). Completing Tasks (14:06): Cách hoàn thành một User Task và truyền vào các biến. End-to-End Flow (16:45): Cách kiểm tra toàn bộ luồng từ đầu đến cuối và kết thúc bằng isEnded().
Bây giờ, hãy cùng nhau hệ thống lại các bước để viết một process test cho một quy trình "Phê duyệt đơn hàng" đơn giản.
Quy trình mẫu: Start Event -> User Task (Phê duyệt) -> Exclusive Gateway -> End Event (Đã duyệt) / End Event (Đã từ chối)
Bước 1: Cấu hình Dependencies
Trong file pom.xml, hãy đảm bảo bạn có các dependency cần thiết cho việc test. camunda-bpm-spring-boot-starter-test đã bao gồm sẵn camunda-bpm-assert.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.camunda.bpm.extension</groupId>
<artifactId>camunda-bpm-process-test-coverage</artifactId>
<version>1.1.1</version> <!-- Hoặc phiên bản tương thích -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Bước 2: Viết Test Class (Happy Path)
Chúng ta sẽ tạo một test class để kiểm tra luồng "đơn hàng được phê duyệt".
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.*;
@SpringBootTest
public class OrderApprovalProcessTest {
@Autowired
private ProcessEngine processEngine;
@Test
public void testHappyPath_OrderApproved() {
// Khởi tạo RuntimeService và TaskService để dễ sử dụng
// BpmnAwareTests cung cấp các hàm tiện ích này
runtimeService = processEngine.getRuntimeService();
taskService = processEngine.getTaskService();
String processDefinitionKey = "order-approval"; // ID của process trong file BPMN
// 1. Bắt đầu một process instance
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey);
// 2. Kiểm tra rằng process instance đang hoạt động và đang chờ ở User Task
assertThat(processInstance).isNotNull();
assertThat(processInstance).isActive();
assertThat(processInstance).isWaitingAt("Task_ApproveOrder"); // ID của User Task
// 3. Lấy task và hoàn thành nó với quyết định "approved"
Task approvalTask = taskService.createTaskQuery()
.processInstanceId(processInstance.getId())
.singleResult();
assertThat(approvalTask).isNotNull();
// Truyền biến 'approved' = true để Gateway đi đúng nhánh
Map<String, Object> variables = Map.of("approved", true);
taskService.complete(approvalTask.getId(), variables);
// 4. Kiểm tra rằng quy trình đã đi qua gateway và kết thúc ở End Event chính xác
assertThat(processInstance).hasPassed("Gateway_Decision"); // ID của Gateway
assertThat(processInstance).hasPassed("EndEvent_Approved"); // ID của End Event
assertThat(processInstance).isEnded();
}
}
Bước 3: Viết Test cho các luồng khác
Tương tự, bạn nên viết một test case riêng cho luồng "từ chối".
@Test
public void testRejectionPath_OrderRejected() {
runtimeService = processEngine.getRuntimeService();
taskService = processEngine.getTaskService();
String processDefinitionKey = "order-approval";
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey);
assertThat(processInstance).isActive().isWaitingAt("Task_ApproveOrder");
Task approvalTask = taskQuery().singleResult();
// Lần này, truyền 'approved' = false
Map<String, Object> variables = Map.of("approved", false);
taskService.complete(approvalTask.getId(), variables);
// Kiểm tra quy trình đi theo nhánh từ chối
assertThat(processInstance).hasPassed("Gateway_Decision");
assertThat(processInstance).hasPassed("EndEvent_Rejected"); // ID của End Event từ chối
assertThat(processInstance).isEnded();
}
Việc viết test cho từng nhánh logic của quy trình giúp bạn đảm bảo mọi kịch bản đều được xử lý đúng như thiết kế.
Tổng kết
Trong bài học hôm nay, bạn đã học được cách áp dụng các kỹ năng kiểm thử phần mềm vào việc phát triển quy trình với Camunda.
- Phân biệt các phạm vi test: Bạn đã hiểu rõ sự khác biệt và mục đích của Unit Test, Process Test, và Integration Test trong bối cảnh BPMN.
- Unit Test một
JavaDelegate: Bạn đã học cách cô lập và kiểm thử logic nghiệp vụ trong mộtJavaDelegatebằng cách sử dụng Mockito và các công cụ giả lập của Camunda, giúp kiểm thử nhanh và hiệu quả. - Process Test một luồng BPMN: Bạn đã nắm vững cách viết các bài test cho toàn bộ quy trình, sử dụng
camunda-bpm-assertđể khởi chạy, tương tác và xác minh trạng thái của một process instance từ đầu đến cuối, bao gồm cả việc kiểm tra các nhánh rẽ của gateway.
Viết test tự động là một khoản đầu tư mang lại lợi ích lâu dài, giúp bạn tự tin thay đổi, tái cấu trúc và mở rộng các quy trình nghiệp vụ của mình.
Xem trước bài học tiếp theo
Với một nền tảng vững chắc về thiết kế module và kiểm thử tự động, chúng ta đã sẵn sàng để giải quyết các bài toán phức tạp hơn trong kiến trúc hướng dịch vụ. Bài học tiếp theo sẽ giới thiệu một pattern rất quan trọng: "Giải thích Saga pattern và vai trò trong việc đảm bảo tính nhất quán dữ liệu". Bạn sẽ thấy việc kiểm thử kỹ lưỡng mà chúng ta vừa học là yếu tố tiên quyết để có thể implement thành công các pattern phức tạp như Saga.