Unit testing workflows with external tasks

Hi, trying to set up unit tests for a given set of workflows that use mostly external tasks. The aim is testing the workflows’ principal function and error behaviour, in a completely mocked in-memory environment.

To emulate the external task client, I use a separate thread polling for tasks like this:

List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(…).topic(…).execute();
…
for (final LockedExternalTask task : tasks) {
    externalTaskService.complete(task.getId(), …);
}

Here’s the complete, simplified code with a small model of three steps, two synchronous service tasks and one external task in between:

With no external tasks (ie, if you comment out line 38) everything runs fine and the process instance ends (line 60).

With the external task, the process instance does not end:

INFO  [pool-1-thread-1 ] MockWorker polling ...
INFO  [main            ] In SignalListener: received start
INFO  [main            ] In Step1, trace=initial
INFO  [pool-1-thread-1 ] MockWorker polling ...
INFO  [pool-1-thread-1 ] In MockWorker Step2, trace=initial -> Step1
INFO  [pool-1-thread-1 ] In Step3, trace=initial -> Step1 -> Step2
INFO  [pool-1-thread-1 ] In SignalListener: received end
INFO  [pool-1-thread-1 ] MockWorker polling ...
INFO  [pool-1-thread-1 ] MockWorker polling ...
INFO  [main            ] ExecutorService shut down
INFO  [pool-1-thread-1 ] MockWorker caught java.lang.InterruptedException
INFO  [pool-1-thread-1 ] Exiting MockWorker

org.opentest4j.AssertionFailedError: 
Expected :true
Actual   :false

I also notice that all steps after the external task are running in the second thread, not in the main thread which makes me wonder:

Is this the right pattern? Is it allowed and safe to do this kind of multi-threaded access to the engine?

Why is it that the process instance is not ended after Step3 is executed?

Extra question: Is there a way to get notified by the engine when a process ends without explicitly modelling the end event?

–John

Hi @ujay68,

When you test your process, you can easily mock your exernal task by completing it in the test code. This library helps you:https://github.com/camunda/camunda-bpm-assert

Here you can write

assertThat(processInstance).isWaitingAt("myExternalTask");
complete(task("myExternalTask"), withVariables("var1", "value1"));

and don’t care about multithreading.

Extra answer: One way in BPMN is to model a message end event: https://docs.camunda.org/manual/7.10/reference/bpmn20/events/message-events/#message-end-event. But it requires some code to send the message.

Also, the last service exection from the external task client can inform you about this. As externalTaskService.complete() runs synchronus until new the process state is saved in the database, the process instance is ended when the call returns.

Hope this helps, Ingo

2 Likes

Hi Ingo_Richtsmeier

I have been trying to follow your advice but I must be missing something. I have defined the process I attached.sample_external.bpmn (3.1 KB).

Then I used:

    <dependency>
        <groupId>org.camunda.bpm.extension</groupId>
        <artifactId>camunda-bpm-assert-root</artifactId>
        <version>1.2</version>
        <type>pom</type>
    </dependency>

to write the following test:

@Test
@Deployment(resources = {"sample_external.bpmn"})
public void start_and_finish_process_external() {
    autoMock("sample_external.bpmn");
    final ProcessInstance processInstance = runtimeService().startProcessInstanceByKey("Sample");
    assertThat(processInstance).isWaitingAt("UserTask_1");
    assertThat(processInstance).isNotEnded();
    complete(task(processInstance));

    assertThat(processInstance).isWaitingAt("Task_External");
    complete(task("Task_External"), withVariables("documentId", 5, "approved", true));
    assertThat(processInstance).hasVariables("documentId");

    assertThat(processInstance).isEnded();
}

When it runs I get the following exception:

java.lang.IllegalArgumentException: Illegal call of complete(task = 'null', variables = '{approved=true, documentId=5}') - both must not be null!
at org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.complete(BpmnAwareTests.java:883) 
at SampleProcessTest.start_and_finish_process_external(SampleProcessTest.java:55)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.camunda.bpm.engine.test.ProcessEngineRule$1.evaluate(ProcessEngineRule.java:172)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.camunda.bpm.spring.boot.starter.test.helper.ProcessEngineRuleRunner.run(ProcessEngineRuleRunner.java:54)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

What am I doing wrong? I suppose I defined the external Task incorrectly…didn’t I? :flushed:
Many thanks in advance for your help!
Best regards,
Fabio

Hi @Fabio_Salvi,

the external tasks are supported since camunda-bpm-assert became part of the Camunda product and no community extension anymore. The maven groupId and version have changed.

Please check https://docs.camunda.org/manual/7.12/user-guide/testing/#camunda-assertions for the current values.

To complete an external task you write complete(externalTask("ExternalTask_id"));.

Hope this helps, Ingo

Hi Ingo,

many thanks!

Best regards,
Fabio