Camunda BPM: Testen von Prozessen mit Java Delegates

Motivation

Das Testen von Prozessdefinitionen ist wichtig und fixiert die Erwartungshaltung an die zu entwickelnde Anwendung. Eine sehr schöne Einführung in das Thema  Testautomatisierung für Camunda BPM basierte Anwendungen gibt das Camunda Webinar: https://network.camunda.org/webinars/24

Ein gängiger Ansatz zum Testen von Prozessdefinitionen ist, dass die Implementierungen von Service Aktivitäten gemockt oder komplett gegen eine eigene für Testzwecke angepasste Implementierung ausgetauscht werden und somit oft ein Prozessdurchlauf im Testkontext erst möglich wird. Für CDI basierte Java Delegates ist dies mit Hilfe von Camunda sehr einfach zu bewerkstelligen.

Wenn jedoch das Projektumfeld es nicht erlaubt auf z.B. die präferierte CDI oder Spring basierte Laufzeitumgebung zurückzugreifen, bleibt oft nur der Weg das Binding zwischen Service Aktivität in der Prozessdefintion und der eigentlichen Implementierung mit Hilfe des konkrete Java Class Namen zu vollziehen. Leider gibt es für diesen Ansatz keine Out-Of-The-Box Unterstützung für Tests.

In diesem Artikel zeigen wir, wie man mit einfachen Mitteln die Camunda Engine erweitern kann, um auch klassische Java Delegates so einfach deren CDI Verwandte testbar zu machen.

Testen mit EL basierten Service Aktivitäten

Dieses Beispiel beschreibt, wie der normale CDI basierte Ansatz für Java Delegates in Tests durch Camunda unterstützt wird:

[sourcecode language=”java”]
@Test
@Deployment(resources = "process.bpmn")
public void testRunningProcess() throws Exception {
// registering a different service task implementation under the el name "fancyService" as used in the process definition
Mocks.register("fancyService", new MockedFancyService());
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(PROCESS_DEFINITION_KEY);

}
[/sourcecode]

Diesen Ansatz hätten wir ebenso gerne für die klassischen Java Delegates benutzt:

[sourcecode language=”java”]
@Test
@Deployment(resources = "process.bpmn")
public void testRunningProcess() throws Exception {
// Registering a different service task implementation under its classname as used in the process definition
// BEWARE This is not working out of the box!
Mocks.register("de.akquinet.camunda.FancyService", new MockedFancyService());
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(PROCESS_DEFINITION_KEY);

}
[/sourcecode]

Leider wird diese Funktionalität nicht Out-Of-The-Box unterstützt.

Camunda BPM unter die Haube geschaut

Um die Mock Funktionalität innerhalb von Unit Tests zu erweitern müssen wir kleine Änderungen an der Konfiguration der Camunda Engine vornehmen. In einem typischen Camunda BPM Projekt ist die Test Konfiguration innerhalb des test source Verzeichnisses zu finden:

[sourcecode title=”/src/test/resources/camunda.cfg.xml” language=”xml”]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans&quot; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"&gt;
<bean id="processEngineConfiguration" class="org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="expressionManager">
<bean class="org.camunda.bpm.engine.test.mock.MockExpressionManager"/>
</property>
<property name="processEnginePlugins">
<list>
<bean class="org.camunda.spin.plugin.impl.SpinProcessEnginePlugin" />
</list>
</property>
</bean>
</beans>
[/sourcecode]

Der MockExpressionManager ist für das Mocking der Delegate Klassen in der Testumgebung verantwortlich. Da für Tests sowieso eine spezielle ProcessEngineConfiguration benötigt wird, können wir diese erweitern um damit auch klassische Java Delegates mocken zu können. Die für die Erzeugung der Instanzen von Java Delegates verantwortliche Komponente ist:

[sourcecode]org.camunda.bpm.engine.ArtifactFactory[/sourcecode]

Aktuell gibt es 2 Implementierungen für diese Komponente, eine für CDI Umgebungen und eine für die klassischen Java Delegates:

[sourcecode]
org.camunda.bpm.engine.cdi.CdiArtifactFactory
org.camunda.bpm.engine.impl.DefaultArtifactFactory
[/sourcecode]

Die DefaultImplementierung nutzt Reflection für die Instanziierung eines Java Delegates. Auf Basis dieser Implementierung stellen wir eine dritte Art der Komponente bereit, die nun auch zusätzliche klassische Java Delegates unterstützt:

[sourcecode title=”de.akquinet.camunda.MockArtifactFactory” language=”java”]
package de.akquinet.camunda;
import org.camunda.bpm.engine.impl.DefaultArtifactFactory;
import org.camunda.bpm.engine.test.mock.Mocks;
public class MockArtifactFactory extends DefaultArtifactFactory {
@Override
public T getArtifact(Class clazz) {
T mockedDelegate = (T) Mocks.get(clazz.getCanonicalName());
if (mockedDelegate == null) {
return super.getArtifact(clazz);
}
return mockedDelegate;
}
}
[/sourcecode]

Die Grundidee ist, zuerst zu testen, ob es unter dem Klassennamen bereits eine registrierte Instanz gibt, wenn nicht wird der Default Mechanismus zur Erzeugung der Instanz benutzt. Damit ist sichergestellt, dass wir echte und gemockte Java Delegates in jeder Kombination innerhalb eines Tests für eine Prozessdefinition nutzen können.
Im nächsten Schritt müssen wir noch die neue Komponente innerhalb der ProcessEngineConfiguration bekanntgeben:

[sourcecode title=”/src/test/resources/camunda.cfg.xml” language=”xml”]
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans&quot; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot; xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"&gt;
<bean id="processEngineConfiguration" class="org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="expressionManager">
<bean class="org.camunda.bpm.engine.test.mock.MockExpressionManager"/>
</property>
<property name="artifactFactory">
<bean class="de.akquinet.camunda.MockArtifactFactory"/>
</property>
<property name="processEnginePlugins">
<list>
<bean class="org.camunda.spin.plugin.impl.SpinProcessEnginePlugin" />
</list>
</property>
</bean>
</beans>
[/sourcecode]

Damit ist es möglich, die Mock Ansatz wie bei CDI basierten Java Delegates nun auch für die klassischen Java Delegates zu nutzen:

[sourcecode language=”java”]
@Test
@Deployment(resources = "process.bpmn")
public void testRunningProcess() throws Exception {
// Registering a different service task implementation under its classname as used in the process definition
Mocks.register("de.akquinet.camunda.FancyService", new MockedFancyService());
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(PROCESS_DEFINITION_KEY);

}
[/sourcecode]

Ein komplett lauffähiges Beispiel haben wir unter GitHub bereitgestellt:  Akquinet GitHub Camunda Examples.