Testing the Camunda BPMN scheme processes in isolation

Testing the Camunda BPMN scheme processes in isolation

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

  • elements describing some business functionality,

  • connection between elements.

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

Greetings! I am Maria, an SDET specialist at the SimbirSoft IT company. In this article, I want to share a successful process testing experience based on the Camunda BPMN scheme.

A brief overview of some elements of notation

For a better understanding of the notation, let’s list some elements that can be used in the scheme. 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 has the following elements:

Fig. 1 – Example of BPMN diagram of a business process

In this example, the following business process is implemented:

  • At the start of the process, a request is created in the Do I have all ingredients element, in which the input data (ingredients, quantity) is transferred.

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

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

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

  • The program receives a message that it is ready in the message intermediate catch event “It’s cooked”.

  • Let’s go to the element Let’s eat.

  • If you do not wait for the message, the timer starts and the order is translated online “Order Online”.

  • The process is complete.

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

Fig. 2 – Information about the element of the BPMN scheme

Why there was a need to test a business scheme

Since the project used Camunda in integration with Spring Boot, the question arose about how to test the microservice in order to have the most complete picture of its correct or incorrect operation. As already mentioned above, it is necessary to check whether the process is going along the way we need in this or that case, as well as integration with external services. Therefore, the team made a strategic decision to develop two types of tests – integration tests and process tests 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 in one way or another.

Manual testing of the scheme

For manual circuit testing, Camunda provides the Cockpit program. It allows you to start processes, view the status of active processes, and view incidents. But it has a number of disadvantages: it does not display the history of completed 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 the token, view incidents. Ex-cam-ad also shows the status of the process in real time.

To test the circuit manually, you need to:

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

  • use the received ex-cam-ad ID, 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 quite a lot of time, and it is not obvious in ex-cam-ad if an error occurred during implementation.

Integration testing

Integration tests in this case were end-to-end tests. The process started after executing the initializing request in RestAssured. Then requests were sent to external services from the application itself, and the execution results were checked in the database.
Integration testing strongly depends 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 the business process in isolation

Testing a business process in isolation belongs to the white box method, because it is necessary to raise the spring context of the program (annotation @SpringBootTestwhich raises the application context) and have access to the code. Therefore, we will create process tests in isolation in the test space of the project. Although such tests are implemented in the same way as unit tests, they should be separated from unit tests and not run together. This can be configured with 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

We wet the 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 – so-called WaitStates).

We start the instance of the process by the key, also passing variable processes to the overloaded method.

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

Peculiarities of writing stubs

All methods of interaction with the WaitStates scheme will be written in a separate class.

A method that stabilizes a delegate takes as input the delegate and the variables that the delegate must place in the process context. The JavaDelegate interface contains an execute method to which DelegateExecution must be passed.

/** 
* Стабирует делегат. 
* @param delegate - делегат 
* @param variables - переменные контекста 
* @return текущий экземпляр 
* @throws Exception 
*/public SchemeObject stubDelegate(final JavaDelegate delegate,                                 
                      final Map 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 waits for an event on the gateway, so a method is needed to 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 install a gateway that expects the It’s coocked event to occur on one path and the timeout to occur on the other 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: the happy path and two paths when the order is executed online.

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 that executes some business logic and puts something 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 the business logic, we need to pass the relevant 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 tool block lists all the checks that the process has started and finished, and that each delegate has transitioned into a valid state.

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

Let’s sum up

In this article, I considered a simple scheme. In practice, business processes are much more complex and contain many different elements. The given approach to testing Camunda is not the only possible one. At the same time, it can be recommended as a system and acceptance test of bpmn schemes on this engine. Also, tests of the process in isolation make it possible to understand whether the process is going correctly in one or another 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/matteobaccan/owner

Thank you for your attention!

Read more author’s materials for SDET specialists from my colleagues on SimbirSoft social networks – VKontakte and Telegram.

Related posts