Why should I test my process models? The short answer is: From a technical point of view, BPMN is a programming language. Therefore, diagrams should be treated like code.

In this blog series “Treat your processes like code – test them!” we’ll step through best practices and procedures to simplify testing and take a closer look at many of the available libraries, including:

When testing models, you’re likely to have questions arise such as:

  • When and how do I use these different libraries?
  • What exactly should be tested – the model and/or the executed code?
  • How do I deal with dependencies to other models?
  • How do I measure my test coverage?

At this point, we would like to recommend the Camunda Best Practices on Testing. In the first posts of this series, we will mainly focus on the first testing scope and write unit tests with Java.

Today’s topic is: Testing Entire Process Paths

The implementation for this post can be found in this GitHub repository.

Let’s take a look at the following order fulfillment process:

“Entire process paths” means testing from start to finish. With complex models, we have often seen that test cases only cover certain parts. In our process, an example would be to test order cancellation separately. The reason you might want to take this approach is because the sequences before and after have already been tested, or the individual test cases become larger and more complex to modify

However, you should always test entire process paths to make sure that:

  • Dependencies in the process are taken into account: Changes to elements that are executed before or after can have side effects. This is especially true for changes to variables that are required for processing.
  • Definition of test cases is simplified: This sounds contradictory at first glance. But we’ve found that considering the whole process scenario simplifies the definition of test cases. Especially when considering only the differing behavior of activities and data in alternative scenarios.
  • Adjustments in the process become easier: Because the entire process is considered, adjustments to the process can be made with greater certainty.

There are, however, cases where it makes sense to test individual activities of a process. An example of this are reusable components. For example, the task “Send cancellation” could be a reusable service task for sending e-mails. However, it should not only be tested in the process for order fulfilment, but also in a separate scope.

With the camunda-bpm-assert library these small, reusable components can be tested very easily. However, testing more complex processes leads to redundant or unclear code. Therefore, the camunda-bpm-assert-scenario is available for testing entire process paths. Now, we will look at a method to test complete process paths easily and efficiently with this library. If you don’t know this project yet, you can have a closer look at the GitHub repository.

Defining the default behavior: First, a default behavior is defined for all elements. This also applies to tasks that are not on the “happy path”, such as the “Cancel Order” task. This means that only the differences in behavior needs to be specified in the individual scenarios.

@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);
}


Identifying activities with different outputs: This step is about identifying activities that provide alternative outputs so that the process takes different paths depending on the output. Often these activities are located before Inclusive or Exclusive Gateways. In our example, this applies to two tasks.

The “Deliver order” activity even has two additional scenarios.

  • The order could not be delivered successfully and it is retried the next day
  • Delivery is not possible and the order must be cancelled.

After the different scenarios have been identified, the specific test cases can now be implemented.

Implementing the test cases: To keep the implementation as simple as possible, only variables relevant to the process flow are considered.

Happy Path
For this purpose, you only have to start the scenario. Afterwards it can be checked whether certain elements or end events have been completed.

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

    verify(testOrderProcess)
            .hasFinished(END_EVENT_ORDER_FULLFILLED);
}

Send Cancellation
We need to override the behavior of the “Check availability” task:

@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
To test this, we need to throw an error in the “Deliver Order” task instead of completing it:

@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
To execute the loop with the timer event and then complete the process, we need to define two different scenarios for the “Deliver Order” task:

@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);
}

Conclusion

With the camunda-bpm-assert-scenario library, it is very easy to test entire process paths. With the approach described above, the defined tests can be implemented efficiently and clearly. But what about code dependencies or integrated call activities? Should these be tested as well or should they be hidden with Mocking Frameworks? We will address this topic in the next post. Stay tuned!

If you’d like to dive deeper into testing topics, check out Testing Cheesecake – Integrate Your Test Reports Easily with FlowCov, presented at CamundaCon LIVE 2020.1 and available free on-demand.

Dominik Horn is Co-Founder of FlowSquad — specialists in process automation and individual software development. Stay in touch with Flowsquad on TwitterGitHub or email — info@flowsquad.io.