Welcome to the next post in our blog series: “Treat your processes like code – test them!”

You can find the last post: “Testing entire process paths” here.

Today’s topic is “Testing process dependencies”. For the execution of a model, there are often additional resources required. This might be source code or the dependency on other models. But how can we deal with this when testing our models? In this post we will take a closer look at the following dependencies:

  • Models: Dependencies to BPMN diagrams referenced by the executed model
  • Code: Dependencies on source code referenced in the BPMN.

We will get to know another library that will help us with testing: camunda-bpm-mockito. The examples for this blog post can be found in this GitHub repository.

In the last post, we took a closer look at a small ordering process and tested it. Now we want to extend it and include additional features that we have to account for in our tests:

  • Another process that is referenced by a Call Activity
  • A Java Delegate that has additional dependencies to a service

Dependencies on other BPMN models

There are several reasons to include BPMN diagrams as Call Activities or to start them from within the code:

  • Reducing complexity: Extensive BPMN models can have a negative impact on the understanding of the actual process flow. Therefore, it is sometimes useful to outsource technical details and particularities to a separate diagram.
  • Reusability of components: As the number of automated processes increases, so does the number of functions that can be used in different places. If these are not just simple service calls, it can make sense to outsource these functions into separate processes.
  • Starting processes: Sometimes it is necessary to start processes asynchronously. This can be the case if further processing steps are to be performed after an instance has ended, without the process having to wait for their completion.

All three cases lead to a dependency on another process model during testing. But how should we deal with this?

Using the referenced model

We could use and test the referenced model in a unit test. However, this is not recommended for the following reasons:

  • The BPMN model must be tested and the test case will become more extensive
  • Reusable components are unnecessarily tested multiple times
  • If there are different return values or errors in the referenced model, to which the process must respond, this leads to an enormous additional effort in the test case
  • The dependencies of the referenced model in the test case must be accounted for
  • Modifications in the referenced model affect the test case of the process

Using another model with the same key

Instead of using the referenced diagram, a custom model with the same key can be deployed, the result of which can be parameterized. This is done with a few lines of code:

BpmnModelInstance modelInstance = Bpmn.createExecutableProcess()
          .id("callActivity")
          .startEvent()
          .serviceTask().camundaResultVariable("result").camundaExpression(result)
          .endEvent()
          .done();

  Deployment deployment = rule.getProcessEngine()
          .getRepositoryService()
          .createDeployment()
          .addModelInstance("callActivity" + ".bpmn", modelInstance)
          .deploy();


For simple models, this is certainly a viable option. There are, however, cases that lead to additional effort – for example, if there are different return values or errors in the referenced model to which the process has to respond.

Mocking the model using camunda-bpm-mockito

Instead of building our own mock of the model, we can use the camunda-bpm-mockito library. This offers the following advantages:

  • Clear tests that focus on the actual process
  • Errors in a used model do not affect the test of the parent process
  • Differences in the behavior of the referenced model can be simulated more easily
  • Shorter execution times for tests

Now let’s take a look at our order process. The delivery is to be outsourced as an independent, reusable process that is referenced as a Call Activity.

We will now reference this delivery process as a Call Activity in the order process. But how do we handle this in our test? There are two tasks for us:

  • Mocking the delivery process in the test
  • Write a separate test for the delivery process

Mocking the delivery process in a test

For this purpose we add the following to the defaultScenario() method:

ProcessExpressions.registerCallActivityMock(DELIVERY_PROCESS_KEY)
                .deploy(rule);

        when(testOrderProcess.runsCallActivity(TASK_DELIVER_ORDER1))
                .thenReturn(Scenario.use(deliveryRequest));


In shouldExecuteOrderCancelled we have to adjust the behavior of the Call Activity Mock to throw an error during execution:

ProcessExpressions.registerCallActivityMock(DELIVERY_PROCESS_KEY)
                .onExecutionDo(execution -> {
                    throw new BpmnError("deliveryFailed");
                })
                .deploy(rule);


We are already done with defining different variants for our called order process – quite simple! There is much more you can do with camunda-bpm-mockito, just give it a try.

Writing a separate test for the delivery process

Next, we create a separate test class for the delivery process and adopt the methods shouldExecuteOrderCancelled and shouldExecuteDeliverTwice.

@Deployment(resources = "deliver-process.bpmn")
public class DeliveryProcessTest {

    public static final String PROCESS_KEY = "deliveryprocess";
    public static final String TASK_DELIVER_ORDER = "Task_DeliverOrder";
    public static final String VAR_ORDER_DELIVERED = "orderDelivered";
    public static final String END_EVENT_DELIVERY_COMPLETED = "EndEvent_DeliveryCompleted";
    public static final String END_EVENT_DELIVERY_CANCELLED = "EndEvent_DeliveryCancelled";

    @Rule
    public ProcessEngineRule rule = new ProcessEngineRule();

    @Mock
    private ProcessScenario testDeliveryProcess;

    @Before
    public void defaultScenario() {
        MockitoAnnotations.initMocks(this);

        //Happy-Path
        when(testDeliveryProcess.waitsAtUserTask(TASK_DELIVER_ORDER))
                .thenReturn(task -> {
                    task.complete(withVariables(VAR_ORDER_DELIVERED, true));
                });
    }

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

        verify(testDeliveryProcess)
                .hasFinished(END_EVENT_DELIVERY_COMPLETED);
    }

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

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

        verify(testDeliveryProcess)
                .hasFinished(END_EVENT_DELIVERY_CANCELLED);
    }

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

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

        verify(testDeliveryProcess, times(2))
                .hasCompleted(TASK_DELIVER_ORDER);
        verify(testDeliveryProcess)
                .hasFinished(END_EVENT_DELIVERY_COMPLETED);
    }
}

Dependencies on source code

Now let’s take a look at how we can deal with code dependencies:

  • Replace all dependencies with mocks
  • Provide the entire context
  • Provide specific classes with dependencies

If all dependencies are replaced by mocks, the test loses its validity. Providing the whole context instead would in turn miss the goal of a unit test, would be very time-consuming for larger applications, and would lead to longer test run times. The solution, therefore, has to be the last point. But which classes should be provided and which should be replaced by mocks?

Let’s look at this example using the Java delegate used in the send cancellation task and add a mailing service:

@Component
public class SendCancellationDelegate implements JavaDelegate {

    private final MailingService mailingService;

    @Autowired
    public SendCancellationDelegate(MailingService mailingService) {
        this.mailingService = mailingService;
    }

    @Override
    public void execute(DelegateExecution delegateExecution) throws Exception {
        //input
        final String customer = (String) delegateExecution.getVariable("customer");

        //processing
        mailingService.sendMail(customer);

        //output
        delegateExecution.setVariable("cancellationTimeStamp", Instant.now().getEpochSecond());
    }
}


This delegate reads a process variable, uses the mailing service to send the cancellation, and writes the time of cancellation back into the process. It is definitely a good idea to execute this delegate during a test case because it increases the significance of the test. Sending the mail, however, is not useful.

In summary: Classes referenced from the diagram should be executed if possible. Their dependencies in turn should be mocked.

For this purpose, we extend the test as follows:

  1. Create a MailingService mock:
@Mock
  private MailingService mailingService;

2. Pass the mock to the delegate:

Mocks.register("sendCancellationDelegate", new SendCancellationDelegate(mailingService));

3. Do nothing when sendMail() is executed:

doNothing().when(mailingService).sendMail(any());

4. Check if the mailing service is invoked:

@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, withVariables(VAR_CUSTOMER, "john"))
              .execute();

      verify(testOrderProcess)
              .hasFinished(END_EVENT_CANCELLATION_SENT);

      //verfiy execution of mailingService
      verify(mailingService, (times(1))).sendMail(any());
      verifyNoMoreInteractions(mailingService);
  }


In complex contexts it can be difficult to keep track of all the mocks used in a test case. Instead, it makes more sense to outsource them to Factory Classes in order to also consider the dependencies among each other.

Conclusion

With the camunda-bpm-mockito library much more is possible. For example, you can mock messages that are to be correlated or you can simulate the result of a Camunda query. All these functions make it easier to test more complex processes.

This blog post was a first step into the testing of process dependencies. Code and models are often closely linked together, which also affects the scope of our test cases. However, the examples and recommendations shown in this post can help to simplify your own tests and reduce the effort.

But how do we know if the tests we have written are sufficient and cover all necessary parts of the process? And how can we monitor this? We will deal with this topic in our next post.

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.

  • Creating a Frictionless Enterprise: Five Fundamentals

    What if you could remove all of the friction in your enterprise, allowing for a streamlined, perfectly aligned flow of information across both internal and external stakeholders? Manuel Sevilla — Chief Digital Officer of Business Services at Capgemini — recently shared his thoughts on this topic at CamundaCon LIVE 2020.2, identifying the framework businesses must adopt to embrace the frictionless attitude. Let’s take a closer look at the five fundamentals enterprises should consider when trying to increase efficiency, get to market faster, and provide users with an enhanced experience.  1. Hyperscale Automation To improve your enterprise’s efficiency, focus on delivering a touchless process — one that uses automation in a way that makes it easier for the end users (and...

    Read more
  • Should data be embedded in business processes?...

    In October, I had the opportunity to lead a session at Camunda Unconference. This was a rather unique event, with an “unconference” format designed specifically for the community; it was most conducive to great peer-to-peer discussions, and exhange of ideas, experiences and learnings. What was also interesting was that the topics for the sessions were sourced from within the community, and each topic was voted in to a final shortlist. The sessions themselves were discussion-led, to encourage collaboration and creativity. Data in Process I had voted for “Data in Process” as one of the topics; it has always intrigued me, and needless to say, I was thrilled at the opportunity to lead the session! I have been consulting for customers building...

    Read more
  • Hacktoberfest Retro

    Hacktoberfest is a wrap! We’re so pleased that we were able to connect with so many of you from around the world. We set out with a goal of increasing the amount of engagement we get in our GitHub repos and you all delivered! From fixing typos to diving head-long into big feature requests, it was inspiring to see the pull requests that came in. Here are a few of my favorite recaps from the month: Two separate community members translated Tasklist into Hindi and Nepali. A third community member translated Admin, Cockpit, Tasklist and our Welcome Message into Brazilian Portuguese… with proper updates for the 7.14 release Another community member closed a 2-year-old feature request for Camunda to support...

    Read more