Testing Camunda BPMN Processes in Isolation

BPMN (Business Process Model and Notation) notations are increasingly used to describe business processes of any subject area of ​​a real business. The result is something between a block diagram and a functional diagram, which includes:

  • elements that describe some business functionality,

  • connections between elements.

Such a scheme can be implemented in program code. And this raises the question – how to check that the software works correctly with the compiled business model when the program code has already been written.

Hello! I'm Maria, SDET specialist at the IT company SimbirSoft. In this article I want to share my successful experience of process testing based on the Camunda BPMN diagram.

Brief overview of some elements of the notation

For a better understanding of the notation, we list some elements that can be used in the diagram. An example of the simplest scheme is taken from open sources (link to the git hub https://github.com/sourabhparsekar/camunda-masala-noodles) and is presented in Fig. 1. This simple business process contains the following elements:

Fig. 1 – Example of a BPMN business process diagram

Fig. 1 – Example of a BPMN business process diagram

In this example, the following business process is implemented:

  • At the start of the process, in the “Do I have all ingredients” element, an application is generated in which the input data (ingredients, quantity) is transmitted.

  • Then in the exclusive gatevay “Can we cook?” it is checked whether everything necessary for cooking is present.

  • If yes, go to the “Let's cook” element and start cooking, if not, transfer the order online in the “Order Online” element.

  • We wait for the ready state in the event based gateway “Is it ready”.

  • A message comes from the program that it is ready in the message intermediate catch event “It's cooked”.

  • Let's move on to the “Let's eat” element.

  • If the message is not received, the timer is triggered and the order is transferred to Order Online.

  • The process is complete.

You can open the diagram for viewing and editing in the Camunda Modeler application. There, in the modeler, you can find out basic information about the circuit element – its ID, type, name, implementation in code, input/output parameters, etc. (Fig. 2).

Fig. 2 – Information about the BPMN diagram element

Fig. 2 – Information about the BPMN diagram element

Why was there a need to test the business scheme?

Since the project used Camunda in integration with Spring Boot, the question arose of how to test the microservice in order to have the most complete picture of its correct or incorrect operation. As mentioned above, it is necessary to check whether the process is following the path we need in a particular case, as well as integration with external services. Therefore, the team made a strategic decision to develop two types of tests – integration and process in isolation. It is worth noting that both of them implemented end-to-end scenarios, that is, from the start of the process to its completion along one path or another.

Manual circuit testing

For manual testing of circuits, Camunda provides the Cockpit application. It allows you to run processes, view the status of active processes, and view incidents. But it has a number of disadvantages: it does not display the history of terminated processes and there is no way to manage the process token.

An alternative to Cockpit is Ex-cam-ad. In it you can view the history of processes, manage a token, and view incidents. Ex-cam-ad also shows the process status in real time.

To test the circuit manually, you need to:

  • initiate the process with a start request, which, if launched successfully, returns the process ID,

  • use the received ID in ex-cam-ad, where you can track the movement of the token according to the scheme in a specific process, and, in case of a fall, view the stack trace in the logs.

This approach takes a lot of time, and in ex-cam-ad it is not obvious if an error occurred during implementation.

Integration testing

Integration tests in our case were end-to-end tests. The process started after executing the initialization request in RestAssured. Then requests were sent to external services from the application itself, and the execution results were checked in the database.
Integration tests are highly dependent on the availability of external services. And integration tests do not show that the software implementation of the scheme works correctly with the business model.

Testing a business process in isolation

Testing a business process in isolation refers to the white box method, because it is necessary to raise the spring context of the application (abstract @SpringBootTestwhich brings up the application context) and have access to the code. Therefore, we will create process tests in isolation in the project test space. Despite the fact that such tests are implemented in the same way as unit tests, you need to separate them from unit tests and not run them together. This can be done by setting up a separate profile (@ActiveProfiles).

/** 
* Abstract base test class.
*/
@SpringBootTest
@ActiveProfiles("bpm-process-test")
public class AbstractProcessTest {    
/**     
  * Интерфейс для освобождения ресурсов.     
  */    
  private AutoCloseable closeable;    
  /**     
  * Мокаем сценарий через api плаформы Camunda.     
  */    
  @Mock    
  protected ProcessScenario processScenario;

Dependencies that we will need for testing (we will test the Camunda 7 platform):

  • camunda-bpm-junit5

  • camunda-bpm-mockito

  • camunda-bpm-spring-boot-starter-test

  • camunda-bpm-assert

  • camunda-bpm-assert-scenario

  • camunda-process-test-coverage-junit5-platform-7

  • camunda-process-test-coverage-spring-test-platform-7

  • camunda-process-test-coverage-starter-platform-7

Mocking ProcessScenario (from the camunda-bpm-assert-scenario library). Its implementation will allow us to determine what should happen during the execution of the elements of our business scheme (userTask, receiveTask, eventBasedGateway – the so-called WaitStates).

We launch the process instance using the key, also passing process variables to the overloaded method.

Scenario handler = Scenario.run(processScenario)
  .startByKey(PROCESS_KEY, variables)        
  .execute();

Features of writing stubs

We will write all methods for interacting with the WaitStates scheme in a separate class.

The method that stabilizes the delegate takes as input the delegate and the variables that the delegate must put into the process context. The JavaDelegate interface contains an execute method, where you need to pass a DelegateExecution.

/** 
* Стабирует делегат. 
* @param delegate - делегат 
* @param variables - переменные контекста 
* @return текущий экземпляр 
* @throws Exception 
*/public SchemeObject stubDelegate(final JavaDelegate delegate,                                 
                      final Map<String, Object> variables)        
  throws Exception {    
  doAnswer(invocationOnMock -> {        
    DelegateExecution execution = invocationOnMock.getArgument(0);        
    execution.setVariables(variables);        
    return null;    
  }).when(delegate).execute(any(DelegateExecution.class));    
  return this;
}

The process is waiting for an event on the gateway, so we need a method that will stabilize it. ProcessScenario contains methods that should handle Camunda events.

public interface ProcessScenario extends Runnable {    
  UserTaskAction waitsAtUserTask(String var1);    
  TimerIntermediateEventAction waitsAtTimerIntermediateEvent(String var1);    
  MessageIntermediateCatchEventAction waitsAtMessageIntermediateCatchEvent(String var1);    
  ReceiveTaskAction waitsAtReceiveTask(String var1);    
  SignalIntermediateCatchEventAction waitsAtSignalIntermediateCatchEvent(String var1);    
  Runner runsCallActivity(String var1);    
  EventBasedGatewayAction waitsAtEventBasedGateway(String var1);    
  ServiceTaskAction waitsAtServiceTask(String var1);    
  SendTaskAction waitsAtSendTask(String var1);    
  MessageIntermediateThrowEventAction waitsAtMessageIntermediateThrowEvent(String var1);    
  MessageEndEventAction waitsAtMessageEndEvent(String var1);    
  BusinessRuleTaskAction waitsAtBusinessRuleTask(String var1);    
  ConditionalIntermediateEventAction waitsAtConditionalIntermediateEvent(String var1);

In our case, we need to stabilize the gateway, which expects the It's coocked event to occur along one path and the timeout to fire along another path of the process.

/** 
* Стабирует событие на гейтвее. 
* @param gateway - id гейтвея 
* @param event - id события 
* @return текущий экземпляр 
*/public SchemeObject sendGatewayEventTrigger(final String gateway, final String event) 
{    
  when(processScenario.waitsAtEventBasedGateway(gateway))            
    .thenReturn(gw -> gw.getEventSubscription(event).receive());    
  return this;
}

/** 
* Стабирует событие на гейтвее, ожидающем time out error. 
* @param gateway - id гейтвея 
* @return текущий экземпляр 
*/
public SchemeObject sendGatewayTimeout(final String gateway) 
{    
  when(processScenario.waitsAtEventBasedGateway(gateway))            
    .thenReturn(gw -> {});    
  return this;
}

Tests

Let's move on to writing tests. In our example in Fig. 1 there are three possible paths: happy path and two paths when order online is executed.

Let's write SpyBean for delegates.

/** 
* Внедряем нужные нам зависимости делегатов спрингового приложения. 
*/
@SpyBean
protected CheckIngredients checkIngredients;
@SpyBean
protected LetUsCook letUsCook;
@SpyBeanp
rotected LetUsEat letUsEat;
@SpyBean
protected OrderOnline orderOnline;

Each delegate has an execute method, in which some business logic is executed and something is put into the application context.

@Service("LetUsEat")
public class LetUsEat implements JavaDelegate {  
  
  public static final String EAT_NOODLES = "Eat Noodles";    
  private final Logger logger = LoggerFactory.getLogger(this.getClass());    
  /**     
  * We will eat what we cooked if it was not burnt     
  *     
  * @param execution : Process Variables will be retrieved from DelegateExecution     
  */    
  @Override    
  public void execute(DelegateExecution execution) {        
    WorkflowLogger.info(logger, EAT_NOODLES, "Veg masala noodles is ready. Let's eat... But first serve it..");        
    WorkflowLogger.info(logger, EAT_NOODLES, "Transfer to a serving bowl and sprinkle a pinch of chaat masala or oregano over the noodles to make it even more flavorful.");        
    if (execution.hasVariable(Constants.CHEESE) && (boolean) execution.getVariable(Constants.CHEESE))            
      WorkflowLogger.info(logger, EAT_NOODLES, "Add grated cheese over it. ");        
    WorkflowLogger.info(logger, EAT_NOODLES, "Serve it hot to enjoy!! ");        
    execution.setVariable(Constants.DID_WE_EAT_NOODLES, true);

As part of stabilization, we are not interested in business logic; we need to pass the appropriate variables to the stubDelegate method.

@Test
@DisplayName("Given we can cook and ready cook" +        
             "When process start then process successful")
public void happyPathTest() throws Exception {    
  schemeObject            
    .stubDelegate(checkIngredients, Map.of(Constants.INGREDIENTS_AVAILABLE, true))            
    .stubDelegate(letUsCook, Map.of(Constants.IS_IT_COOKING, true))            
    .sendGatewayEventTrigger("IsItReady", "IsReady")            
    .stubDelegate(letUsEat, Map.of(Constants.DID_WE_EAT_NOODLES, true));    
  Scenario handler = Scenario.run(processScenario).startByKey(PROCESS_KEY, variables)            
    .execute();    
  assertAll(            
    () -> assertThat(handler.instance(processScenario)).isStarted(),            
    () -> verify(processScenario).hasCompleted("Start_Process"),            
    () -> verify(processScenario).hasCompleted("CheckIngredients"),            
    () -> verify(processScenario).hasCompleted("CanWeCook"),            
    () -> verify(processScenario).hasCompleted("LetsCook"),            
    () -> verify(processScenario).hasCompleted("IsReady"),            
    () -> verify(processScenario).hasCompleted("LetUsEat"),            
    () -> verify(processScenario).hasCompleted("End_Process"),            
    () -> assertThat(handler.instance(processScenario)).isEnded()    
  );

The assertAll block lists checks that the process has started and completed, and that each delegate has entered the hasCompleted state.

The result of the tests is saved in the form of a report below with visualization of the path traveled and the percentage of circuit coverage.

Let's sum it up

In this article I looked at a fairly simple scheme. In practice, business processes are much more complex and contain many different elements. The presented approach to testing Camunda is not the only one possible. At the same time, it can be recommended as system and acceptance testing of bpmn circuits on this engine. Also, process tests in isolation make it possible to understand whether the process is proceeding correctly in a particular case and that the software implementation of the scheme works correctly with the business model.

Useful links

https://docs.camunda.io/docs/components/best-practices/development/testing-process-definitions/

https://github.com/camunda/camunda-bpm-platform/tree/master/test-utils/assert

https://github.com/camunda-community-hub/camunda-process-test-coverage

https://github.com/camunda-community-hub/camunda-platform-scenario

http://www.awaitility.org

https://github.com/matteobacan/owner

Thank you for your attention!
Read more original materials for SDET specialists from my colleagues on SimbirSoft’s social networks –
VKontakte And Telegram.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *