Ursprünglich am 20.11.2020 auf dem offiziellen Camunda-Blog gepostet: https://camunda.com/blog/2020/11/testing-process-dependencies/
Heute geht es um das Thema “Testing process dependencies”. Für die Ausführung eines Modells werden häufig weitere Ressourcen benötigt. Dabei kann es sich um Quellcode oder die Abhängigkeit zu anderen Modellen handeln. Doch wie gehen wir damit beim Testen unserer Modelle um?
In diesem Post werden wir die folgenden Abhängigkeiten genauer unter die Lupe nehmen:
Dabei werden wir eine weitere Library kennenlernen, die uns das Testen erleichtert: camunda-bpm-mockito. Die Beispiele für diesen Blogbeitrag findet ihr in diesem GitHub-Repository.
Im letzten Post haben wir uns einen kleinen Bestell-Prozess näher angeschaut und getestet. Diesen könnt ihr hier finden. In diesem Teil wollen wir das nun weiter ausbauen und um Funktionalitäten erweitern, die wir beim Testen berücksichtigen müssen:
Es gibt verschiedene Gründe dafür, BPMN-Diagramme als Call Activities einzubinden oder aus dem Code heraus zu starten:
Alle drei Fälle führen dazu, dass wir beim Testen eine Abhängigkeit auf ein weiteres Prozessmodell haben. Doch wie sollen wir damit umgehen?
Wir können das referenzierte Modell in einem Unit-Test verwenden und testen. Dies ist jedoch aus den folgenden Gründen nicht zu empfehlen:
Anstatt das referenzierte Diagramm zu verwenden, kann ein eigenes Modell mit dem gleichen Key deployed werden, dessen Ergebnis parametrisiert werden kann. Dies ist mit wenigen Zeilen Code erledigt:
BpmnModelInstance modelInstance = Bpmn.createExecutableProcess() .id("callActivity") .startEvent() .serviceTask().camundaResultVariable("result").camundaExpression(result) .endEvent() .done(); Deployment deployment = rule.getProcessEngine() .getRepositoryService() .createDeployment() .addModelInstance("callActivity" + ".bpmn", modelInstance) .deploy();
Für einfache Modelle ist dies durchaus ein praktikabler Weg. Es gibt jedoch Fälle, die wiederum zu Mehraufwand führen. Besonders dann, wenn es im referenzierten Modell unterschiedliche Rückgabewerte oder Fehler gibt, auf die im Prozess reagiert werden muss.
camunda-bpm-mockito
mockenAnstatt einen eigenen Mock des Modells zu bauen, kann hierfür die camunda-bpm-mockito library verwendet werden. Dies bringt folgende Vorteile:
Werfen wir nun einen Blick auf unseren Bestellprozess. Die Lieferung soll als eigenständiger, wiederverwendbarer Prozess ausgelagert werden, der als Call Activity referenziert wird.
Diesen Lieferprozess referenzieren wir nun als Call Activity im Bestellprozess. Doch wie gehen wir damit nun in unserem Test um? Es gibt zwei Aufgaben für uns:
Hierzu ergänzen wir die defaultScenario()-Methode wie folgt:
ProcessExpressions.registerCallActivityMock(DELIVERY_PROCESS_KEY) .deploy(rule); when(testOrderProcess.runsCallActivity(TASK_DELIVER_ORDER1)) .thenReturn(Scenario.use(deliveryRequest));
Im shouldExecuteOrderCancelled müssen wir das Verhalten des Call Activity-Mocks anpassen, um bei der Ausführung einen Fehler zu werfen:
ProcessExpressions.registerCallActivityMock(DELIVERY_PROCESS_KEY) .onExecutionDo(execution -> { throw new BpmnError("deliveryFailed"); }) .deploy(rule);
Und schon haben wir unterschiedliche Varianten für unseren aufgerufenen Bestellprozess definiert - ziemlich einfach! Mit camunda-bpm-mockito ist noch vieles mehr möglich, ausprobieren lohnt sich.
Als nächstes erstellen wir für den Lieferprozess noch eine eigene Testklasse und übernehmen die Methoden shouldExecuteOrderCancelled und shouldExecuteDeliverTwice.
@Deployment(resources = "deliver-process.bpmn") public class DeliveryProcessTest { public static final String PROCESS_KEY = "deliveryprocess"; public static final String TASK_DELIVER_ORDER = "Task_DeliverOrder"; public static final String VAR_ORDER_DELIVERED = "orderDelivered"; public static final String END_EVENT_DELIVERY_COMPLETED = "EndEvent_DeliveryCompleted"; public static final String END_EVENT_DELIVERY_CANCELLED = "EndEvent_DeliveryCancelled"; @Rule public ProcessEngineRule rule = new ProcessEngineRule(); @Mock private ProcessScenario testDeliveryProcess; @Before public void defaultScenario() { MockitoAnnotations.initMocks(this); //Happy-Path when(testDeliveryProcess.waitsAtUserTask(TASK_DELIVER_ORDER)) .thenReturn(task -> { task.complete(withVariables(VAR_ORDER_DELIVERED, true)); }); } @Test public void shouldExecuteHappyPath() { Scenario.run(testDeliveryProcess) .startByKey(PROCESS_KEY) .execute(); verify(testDeliveryProcess) .hasFinished(END_EVENT_DELIVERY_COMPLETED); } @Test public void shouldExecuteOrderCancelled() { when(testDeliveryProcess.waitsAtUserTask(TASK_DELIVER_ORDER)).thenReturn(task -> { taskService().handleBpmnError(task.getId(), "DeliveryCancelled"); }); Scenario.run(testDeliveryProcess) .startByKey(PROCESS_KEY) .execute(); verify(testDeliveryProcess) .hasFinished(END_EVENT_DELIVERY_CANCELLED); } @Test public void shouldExecuteDeliverTwice() { when(testDeliveryProcess.waitsAtUserTask(TASK_DELIVER_ORDER)).thenReturn(task -> { task.complete(withVariables(VAR_ORDER_DELIVERED, false)); }, task -> { task.complete(withVariables(VAR_ORDER_DELIVERED, true)); }); Scenario.run(testDeliveryProcess) .startByKey(PROCESS_KEY) .execute(); verify(testDeliveryProcess, times(2)) .hasCompleted(TASK_DELIVER_ORDER); verify(testDeliveryProcess) .hasFinished(END_EVENT_DELIVERY_COMPLETED); } }
Schauen wir nun, wie wir mit Code-Abhängigkeiten umgehen können:
Werden alle Abhängigkeiten durch Mocks ersetzt, verliert der Test an Aussagekraft. Stattdessen den kompletten Kontext bereitzustellen, würde wiederum am Ziel eines Unit-Tests vorbeigehen, wäre bei größeren Anwendungen sehr aufwendig und würde zu längeren Test-Laufzeiten führen. Die Lösung liegt somit im letzten Punkt. Doch welche Klassen sollen bereitgestellt und welche durch Mocks ersetzt werden?
Schauen wir uns dieses Beispiel anhand des Java-Delegates an, das im Send Cancellation-Task verwendet wird, und ergänzen es um einen Mailing-Service:
@Component public class SendCancellationDelegate implements JavaDelegate { private final MailingService mailingService; @Autowired public SendCancellationDelegate(MailingService mailingService) { this.mailingService = mailingService; } @Override public void execute(DelegateExecution delegateExecution) throws Exception { //input final String customer = (String) delegateExecution.getVariable("customer"); //processing mailingService.sendMail(customer); //output delegateExecution.setVariable("cancellationTimeStamp", Instant.now().getEpochSecond()); } }
Dieses Delegate liest eine Prozessvariable, verwendet den Mailing-Service, um die Stornierung zu versenden und schreibt den Zeitpunkt der Stornierung zurück in den Prozess. Es ist durchaus von Vorteil, dieses Delegate während eines Testfalls auszuführen, denn es erhöht die Aussagekraft des Tests. Das Versenden der Mail ist jedoch nicht sinnvoll.
Zusammengefasst: Klassen, die aus dem Diagramm referenziert werden, sollen wenn möglich ausgeführt werden. Deren Abhängigkeiten wiederum gilt es zu mocken.
Hierfür erweitern wir den Test wie folgt:
@Mock private MailingService mailingService;
Mocks.register("sendCancellationDelegate", new SendCancellationDelegate(mailingService));
doNothing().when(mailingService).sendMail(any());
@Test public void shouldExecuteCancellationSent() { when(testOrderProcess.waitsAtUserTask(TASK_CHECK_AVAILABILITY)).thenReturn(task -> { task.complete(withVariables(VAR_PRODUCTS_AVAILABLE, false)); }); Scenario.run(testOrderProcess) .startByKey(PROCESS_KEY, withVariables(VAR_CUSTOMER, "john")) .execute(); verify(testOrderProcess) .hasFinished(END_EVENT_CANCELLATION_SENT); //verfiy execution of mailingService verify(mailingService, (times(1))).sendMail(any()); verifyNoMoreInteractions(mailingService); }
Bei komplexen Kontexten kann es schwierig werden, den Überblick über alle in einem Testfall verwendeten Mocks zu behalten. In diesem Fall ist es sinnvoller, diese in Factory-Klassen auszulagern, um auch die Abhängigkeiten untereinander zu berücksichtigen.
Mit der camunda-bpm-mockito library ist noch vieles mehr möglich. Es können bspw. Messages gemockt werden, die beim Ausführen des Modells korreliert werden sollen oder es ist möglich, das Ergebnis einer Camunda Query zu simulieren. All diese Funktionen erleichtern das Testen von komplexeren Prozessen.
Dieser Blog-Post war eine Einführung in das Testen von Prozessabhängigkeiten. Code und Modelle sind häufig eng miteinander verknüpft, was sich auch auf den Umfang der Testfälle auswirkt. Die hier gezeigten Beispiele und Empfehlungen können jedoch dabei helfen, die eigenen Tests zu vereinfachen und den Aufwand zu reduzieren.
Aber woher wissen wir, ob die geschriebenen Tests ausreichend sind und alle notwendigen Teile des Prozesses abdecken? Und wie können wir das kontrollieren? Mit diesem Thema beschäftigen wir uns in unserem nächsten Post.
Rechtliches