Vollständige Prozesspfade testen

Warum sollte ich meine Modelle testen? Die kurze Antwort ist: Aus technischer Sicht ist BPMN eine Programmiersprache. Deshalb sollten die Diagramme wie Code behandelt werden. Dafür steht inzwischen eine Vielzahl an Bibliotheken bereit, die das Testen vereinfachen.

Ursprünglich am 27.10.2020 auf dem offiziellen Camunda Blog gepostet: https://camunda.com/blog/2020/10/testing-entire-process-paths/

Viele dieser Bibliotheken werden wir genauer unter die Lupe nehmen, darunter:

Beim Testen von Modellen stellen sich häufig Fragen wie:

  • Wann und wie setze ich die verschiedenen Bibliotheken ein?
  • Was soll genau getestet werden - das Modell und der ausgeführte Code?
  • Wie gehe ich mit Abhängigkeiten zu anderen Modellen um?
  • Wie messe ich meine Testabdeckung?

Mit diesen Fragen beschäftigt sich unsere neue Blogreihe “Treat your processes like code - test them!” Wir wollen Best Practices und Vorgehensweisen sammeln, um das Testen zu vereinfachen.

An dieser Stelle gibt es eine kleine Leseempfehlung - die Camunda Best Practices zum Thema Testing. Wir werden uns in den ersten Posts überwiegend im ersten Test-Scope bewegen und Unit Tests mit Java schreiben.

Testen vollständiger Prozesspfade

Die Implementierung für diesen Post liegt in diesem GitHub-Repository.

Schauen wir uns dazu folgenden Prozess zur Auftragsabwicklung an:

“Vollständige Prozesspfade” bedeutet von Anfang bis Ende zu testen. Bei komplexen Modellen haben wir schon häufig gesehen, dass Testfälle nur Teile abdecken. In unserem Prozess wäre ein Beispiel dafür, wenn der Abbruch der Bestellung und die Stornierung separat getestet werden.

Der Grund für dieses Vorgehen kann sein, dass die Abläufe davor und danach schon getestet sind oder die einzelnen Testfälle dann viel größer und aufwendiger in der Anpassungen werden.

Aus folgenden Gründen sollten jedoch immer vollständige Prozesspfade getestet werden:

  • Abhängigkeiten im Prozess werden berücksichtigt: Änderungen an Elementen, die im Prozess davor oder danach durchlaufen werden, können Auswirkungen haben. Insbesondere bei Änderungen an Variablen, die zur Abarbeitung benötigt werden.
  • Definition von Testfällen wird vereinfacht: Das klingt im ersten Moment widersprüchlich. Doch haben wir die Erfahrung gemacht, dass die Betrachtung vollständiger Prozessfalle, die Definition von Testfällen vereinfacht. Insbesondere, wenn in alternativen Szenarien, nur das abweichende Verhalten von Aktivitäten und Daten betrachtet wird.
  • Anpassungen im Prozess werden leichter: Dadurch, dass immer der gesamte Ablauf betrachtet wird, können Anpassungen am Prozess mit größerer Sicherheit gemacht werden.

Es gibt jedoch auch Fälle, in denen es durchaus sinnvoll ist, einzelne Aktivitäten eines Prozesses zu testen. Ein Beispiel hierfür sind wiederverwendbare Komponenten. Der Task “Send cancellation” könnte bspw. ein wiederverwendbarer Service Tasks zum Senden von E-Mails sein. Dieser sollte jedoch dann nicht isoliert im Prozess zur Auftragsabwicklung getestet werden, sondern in einem eigenen Scope.

Mit der camunda-bpm-assert Bibliothek können diese kleinen, wiederverwendbaren Komponenten sehr einfach getestet werden. Das Testen von komplexeren Abläufe führt jedoch zu redundantem oder unübersichtlichen Code.

Für das Testen vollständiger Prozesspfade steht deshalb die camunda-bpm-assert-scenario bereit. Schauen wir uns nun ein Vorgehen an, wie mit dieser Library ganze Prozesspfade einfach und effizient getestet werden können. Wer diese Projekt noch nicht kennt, kann zunächst einen genauen Blick ins GitHub Repository werfen.

Standardverhalten definieren: Zunächst wird für alle Elemente ein Standardverhalten definiert. Dies gilt auch für Aufgaben, die nicht auf dem “Happy-Path” liegen, wie der “Cancel Order” Task. Dadurch muss in den einzelnen Szenarien nur noch die Abweichung neu definiert werden.

@Before
public void defaultScenario() {
    MockitoAnnotations.initMocks(this);
    Mocks.register("sendCancellationDelegate", new SendCancellationDelegate());

    //Happy-Path
    when(testOrderProcess.waitsAtUserTask(TASK_CHECK_AVAILABILITY))
            .thenReturn(task -> {
                task.complete(withVariables(VAR_PRODUCTS_AVAILABLE, true));
            });

    when(testOrderProcess.waitsAtUserTask(TASK_PREPARE_ORDER))
            .thenReturn(TaskDelegate::complete);

    when(testOrderProcess.waitsAtUserTask(TASK_DELIVER_ORDER))
            .thenReturn(task -> {
                task.complete(withVariables(VAR_ORDER_DELIVERED, true));
            });

    //Further Activities
    when(testOrderProcess.waitsAtUserTask(TASK_CANCEL_ORDER))
            .thenReturn(TaskDelegate::complete);
}

Aktivitäten mit weiterem Verhalten erkennen: In diesem Schritt geht es darum, Aktivitäten zu erkennen, die einen alternativen Output liefern, sodass der Prozess einen weiteren Pfad nimmt. Häufig stehen diese Aktivitäten vor Inklusiven oder Exkulsiven Gateways. In unserem Beispiel trifft das auf zwei Tasks zu.

Der Task “Deliver order” hat sogar zwei weitere Szenarien.

  • Die Bestellung konnte nicht erfolgreich zugestellt werden und es wird am nächsten Tag nochmals versucht
  • Die Zustellung ist nicht möglich und die Bestellung muss storniert werden.

Nachdem die unterschiedlichen Szenarien erkannt wurde, können nun die spezifischen Testfälle implementiert werden.

Implementierung der Testfälle: Um die Implementierung so einfach wie möglich zu halten, werden nur Variablen berücksichtigt, die für den Ablauf des Prozesses relevant sind.

Happy Path
Hierfür muss lediglich das Scenario gestartet werden. Danach kann geprüft werden ob bestimmte Elemente oder End Events abgeschlossen wurden.

@Test
public void shouldExecuteHappyPath() {
    Scenario.run(testOrderProcess)
            .startByKey(PROCESS_KEY)
            .execute();

    verify(testOrderProcess)
            .hasFinished(END_EVENT_ORDER_FULLFILLED);
}

Send Cancellation
Hierfür muss der Task “Check availability” überschrieben werden:

@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)
            .execute();

    verify(testOrderProcess)
            .hasFinished(END_EVENT_CANCELLATION_SENT);
}

Cancel Order
Hierfür ist es notwendig, einen Error im Task “Deliver Order” zu werfen, anstatt diesen abzuschließen.

@Test
public void shouldExecuteOrderCancelled() {
    when(testOrderProcess.waitsAtUserTask(TASK_DELIVER_ORDER)).thenReturn(task -> {
        taskService().handleBpmnError(task.getId(), "OrderCancelled");
    });

    Scenario.run(testOrderProcess)
            .startByKey(PROCESS_KEY)
            .execute();
            
    verify(testOrderProcess)
            .hasCompleted(TASK_CANCEL_ORDER);
    verify(testOrderProcess)
            .hasFinished(END_EVENT_ORDER_CANCELLED);
}

Deliver twice
Um den Loop mit dem Timer Event zu durchlaufen und anschließend den Prozess abzuschließen, müssen für den Task “Deliver Order” zwei verschiedene Szenarien definiert werden.

@Test
public void shouldExecuteDeliverTwice() {
    when(testOrderProcess.waitsAtUserTask(TASK_DELIVER_ORDER)).thenReturn(task -> {
        task.complete(withVariables(VAR_ORDER_DELIVERED, false));
    }, task -> {
        task.complete(withVariables(VAR_ORDER_DELIVERED, true));
    });

    Scenario.run(testOrderProcess)
            .startByKey(PROCESS_KEY)
            .execute();

    verify(testOrderProcess, times(2))
            .hasCompleted(TASK_DELIVER_ORDER);
    verify(testOrderProcess)
            .hasFinished(END_EVENT_ORDER_FULLFILLED);
}

Fazit

Mit der camunda-bpm-assert-scenario Bibliothek ist es sehr einfach vollständige Prozesspfade zu testen. Mit dem zuvor beschrieben Vorgehen, lassen sich die definierten Tests effizient und übersichtlich umsetzen. Aber was ist mit Code Abhängigkeiten oder eingebunden Call Activities? Sollen diese mitgetestet werden oder mit Mocking Frameworks versteckt werden? Diesem Thema widmen wir uns im nächsten Post. Bleibt dran!

Falls euch weitere Themen im Testing Bereich interessieren oder ihr eigene Erfahrung teilen wollt, schreibt uns!