Testing Process Dependencies

By
person behind laptop
  • Blog
  • >
  • Testing Process Dependencies
TOPICS

30 Day Free Trial

Bring together legacy systems, RPA bots, microservices and more with Camunda

Sign Up for Camunda Content

Get the latest on Camunda features, events, top trends, and more.

TRENDING CONTENT

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
ordering process

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.

ordering process

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

      //verify 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 Miragon — specialists in process automation and individual software development. Stay in touch with Miragon on Twitter or GitHub.

Camunda Developer Community

Join Camunda’s global community of developers sharing code, advice, and meaningful experiences

Try All Features of Camunda

Related Content

Learn how to get started with automated underwriting, what the benefits are and why automation in underwriting is important.
An integral part of process orchestration is process automation—get those repeatable, well-understood tasks that don't require complex decision-making on autopilot!
Enhance your business's operational efficiency with business process management, and streamline your workflows to reduce cost and minimize risk.