Camunda und JUnit 5

Schon im Beitrag Stammbaumprüfung mit BPMN wurde der Einsatz von BPMN Workflows in einer Workflow-Engine vorgestellt. Im damaligen Beitrag wurde die Workflow-Engine Flowable verwendet. In diesem Beitrag dreht es sich um die Workflow-Engine Camunda.

Wer eine neuen Workflow erstellt hat, möchte diesen natürlich auch gerne im Vorfeld testen. Dafür stellt auch Camunda eine JUnit 5 Unterstützung bereit. Ein erster Test für einen neuen Workflow ist im folgenden Beispiel dargestellt.

import static org.camunda.bpm.engine.test.assertions.ProcessEngineTests.runtimeService;

@ExtendWith(ProcessEngineExtension.class)
class AnchestorAddWorkflowTest {
  @Test
  @Deployment(resources = "ancestors-add.bpmn")
  void testHappyPath() {
    ProcessInstance processInstance = runtimeService().startProcessInstanceByKey("ancestor-add", singletonMap("id", "GINA:GEBDAS-1139353466"));
    assertTrue(processInstance.isEnded());
  }
}

Über die @ExtendWith Annotation stellt Camunda alle notwendigen Ressourcen, insbesondere die ProcessEngine bereit. Im eigentlichen Test wird über die @Deployment Annotation der zu testende Workflow bereitgestellt. Im Test wird dann eine Instanz des Prozesses gestartet und geprüft, ob die Verarbeitung erfolgreich durchlaufen wurde.

Unschön in diesem Beispiel sind nur zwei Dinge. Der RuntimeService wird über eine statische Methode geholt und die @Deployment Annotation kann nur an einer Methode stehen. Die folgende Meta-Annotation ist daher leider noch nicht möglich.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Test
@Deployment
public @interface CamundaTest {
  @OverridesAttribute(constraint = Deployment.class )
  String[] resources();
}

Die Parameter-Injektion wird zwar schon unterstützt, bislang aber nur für den ProcessEngine. Das Beispiel sieht damit etwas gefälliger aus. Schöner wäre aber der direkte Zugriff auf den RuntimeService.

@ExtendWith(ProcessEngineExtension.class)
class AnchestorAddWorkflowTest {
  @Test
  @Deployment(resources = "ancestors-add.bpmn")
  void testHappyPath(ProcessEngine engine) {
    ProcessInstance processInstance = engine.getRuntimeService().startProcessInstanceByKey("ancestor-add", singletonMap("id", "GINA:GEBDAS-1139353466"));
    assertTrue(processInstance.isEnded());
  }
}

Für den Injekten des RuntimeService muss momentan noch eine eigene Lösung genutzt werden. Dafür wird die ProcessEngineExtension erweitert und die Methoden supportsParameter und resolveParameter überschrieben.

public class CamundaProcessEngineExtension extends ProcessEngineExtension {

  private final Map<Class<?>, Supplier<Object>> SUPPLIERS = Map.of(
      ProcessEngine.class, this::getProcessEngine,
      RuntimeService.class, () -> this.getProcessEngine().getRuntimeService());

  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    return List.of(ProcessEngine.class, RuntimeService.class).contains(parameterContext.getParameter().getType());
  }

  public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
      throws ParameterResolutionException {
    Class<?> type = parameterContext.getParameter().getType();
    return SUPPLIERS.getOrDefault(type,
        () -> {throw new ParameterResolutionException ("cannot find instance of " + type);}).get();
  }
}

In diesem Beispiel wird, neben der schon unterstützen Klasse ProcessEngine, auch noch die Klasse RuntimeService bereitgestellt. Für den RuntimeService wird auf der aktuellen ProcessEngine, über einen Supplier, die Methode getRuntimeService() aufgerufen. Sollte kein passender Supplier gefunden werden, dann wird in diesem Fall eine ParameterResolutionException geworfen.

Damit der RuntimeService injected werden kann, muss nur die Extension ausgetauscht werden und der neue Parameter verwendet werden.

@ExtendWith(ProcessEngineExtension.class)
class AnchestorAddWorkflowTest {
  @Test
  @Deployment(resources = "ancestors-add.bpmn")
  void testHappyPath(RuntimeService service) {
    ProcessInstance processInstance = service.startProcessInstanceByKey("ancestor-add", singletonMap("id", "GINA:GEBDAS-1139353466"));
    assertTrue(processInstance.isEnded());
  }
}

Obwohl einige Kleinigkeiten bei der JUnit 5 Unterstützung noch nicht ganz rund sind, ist das Testen von BPMN Workflows in Camunda eine gelungene Sache.

Leave a Comment