Привет! Продолжаем делиться опытом о том, как можно автоматизировать бизнес-процессы в банке с помощью Camunda. В прошлой статье мы рассказали об одном из проектов, который создали на базе Spring Boot с подключенной Camunda в качестве библиотеки. В этом материале расскажем об особенностях его тестирования. 

Поговорим про тестирование моделей процессов Camunda, приведем примеры кода, конфигураций для запуска процессов в тестовой среде, а также поделимся best practice и некоторыми библиотеками, которые значительно облегчают тестирование процессов. Основная задача данного туториала – сэкономить время и упростить задачу командам разработчиков, которые создают приложения на SpringBoot и Camunda, а также тем, кто хочет начать покрывать модели процессов автотестами.

Camunda: сферы применения

Camunda – это платформа для автоматизации бизнес-процессов. Эту систему используют крупные компании во всем мире, где важна масштабируемость, надёжность, производительность. Большое распространение Camunda получила благодаря тому, что имеет открытый исходный код, реализует стандарты BPMN, поддерживает Java иимеет интеграцию со Spring/Spring Boot.Чаще всего Camunda используется в ритейле, где одним из центральных бизнес-объектов является клиентский заказ и его необходимо координировать с доставкой и оплатой.

Также Camunda широко применяется в банковской сфере. Она позволяет, например, строить сервисы оформления и выдачи банковских карт, где процесс предполагает оформление заявки, многочисленные взаимодействия с core-системами банка или большое количество проверок клиента.

Центральные сервисы в таких проектах строятся на BPMN-схемах. Это своего рода модель бизнес-процесса, созданная при помощи Business Process Management Notation – системы условных обозначений, имеющих графическое представление и описание в xml. К слову, BPMN – глобальный стандарт моделирования процессов и одна из важных составляющих для успешной реализации ИТ-проекта.

Возможные ошибки при создании схемы

С технической точки зрения BPMN схож с языком программирования высокого уровня. Он представляет собой инструкции в графическом виде и имеет свою семантику. Часто BPMN-схемы содержат большое количество ветвлений процесса и выполняемых activity: 

  • service-таски (исполняющие делегатный java-код),

  • user-таски (принимающие действия менеджера), 

  • многочисленные гейтвеи (в т.ч. параллельные, условные, event-based и т.д.),

  • скрипты, boundary или промежуточные перехватчики событий. 

Схемы могут содержать и отдельные изолированные подпроцессы со своим контекстом. В таком многообразии элементов с запрограммированными ветвлениями могут возникать ошибки и баги. Модель процесса – неотъемлемая часть приложения, поэтому относиться к ней нужно также, как к коду, и для обеспечения качества покрывать автотестами.

При проектировании схемы разработчик может столкнуться со следующими наиболее часто встречающимися ошибками:

  • Неверно работающий gateway, boundary/intermediate event и др. Например, выражение содержит ошибку, и процесс не доходит до конечного события - падает в инцидент или останавливается. Событие, ошибка или сообщение не перехватывается соответствующей activity.

  • Процесс направляется по незапланированному пути. В этому случае процесс может успешно доходить до конца и завершаться. Но если проанализировать пройденный путь, то на практике нужные activity не были выполнены, поскольку процесс прошел по неверному направлению.

  • Проблема несогласованности контекста переменных. Если в определенной точке процесса в контексте нет необходимой переменной, а service-таск или gateway требует ее наличия и запрашивает из контекста, то процесс может упасть в инцидент. Один service-task может класть в контекст переменную, а другой - ее запрашивать. Если нарушен порядок расположения этих activity, процесс будет падать в инцидент.

  • Инциденты в уже запущенных процессах. Они связаны с проблемой внесения изменений в схему. Некоторые бизнес-процессы могут идти несколько дней. Если в этом случае изменить схему и бизнес-логику делегатов, возникнет ситуация, когда уже запущенные процессы работают на старой версии схемы, но с новой логикой делегатов. Конечно, это больше вопрос про миграцию процессов и версионирование java-делегатов. Но, если иметь возможность  запускать в тестах процесс с некоторой средней точки, задав определенный пререквизит, можно частично предупредить эти негативные сценарии.

Большинство этих проблем можно выявить в ходе локального ручного прогона инстанса процесса. Но ветвление может быть очень обширным, и ручная отладка всех путей процесса займет очень много времени. В этом случае не обойтись без автотестов.

Что нужно протестировать

Как правило, на схеме присутствует большое количество service-тасок, привязанных к делегатному java-коду. Бизнес-логика этих делегатов не особо важна для тестирования модели процесса, они должны быть покрыты своими юнит-тестами. В нашем случае точно нужно протестировать:

  • Схему, модель бизнес-процесса. Мы должны быть уверены, что процесс стартует и завершается. HappyPath тестируется в первую очередь. Бизнес-логика может быть замокана.

  • Процесс в изоляции. Если приложение вызывает внешние сервисы, то они застабированы, это задача интеграционных тестов.

  • Проверяем, что процесс направляется gateway и boundary-ивентами по задуманному пути, в идеале протестировать все возможные варианты поведения перенаправления процесса. Это нужно для того, чтобы на схеме не было недостижимых ненужных участков.

  • Проверяем согласованность контекста. Сама по себе оттестированная голая схема не особо полезна. Очень важно иметь тесты, проверяющие согласованность контекста: в момент вызова процессом делегат должен иметь необходимые для его работы переменные в контексте Camunda.

Какие подходы к тестированию можно использовать

Для тестирования схемы нам нужно иметь возможность запускать процесс в тестовой среде. Для это нам потребуется ProcessEngine – основной компонент Camunda. BPMN Engine – это непосредственно движок, который отвечает за интерпретации BPMN в объекты JAVA, а также за сохранение объектов в базе и реализацию других вещей (типа листенеров активностей, таймеров), которые крутятся вокруг процессов. 

ProcessEngine предоставляет доступ также к другим сервисам Camunda:

  • RuntimeService: создание Deployments, старт инстансов процесса.

  • TaskService: операции для управления User-тасками, назначение исполнителя, выполнение и т.д.

  • IdentityService: управление пользователями и группами, отношениями между ними.

  • ManagementService: предоставляет операции для управления движком.

  • HistoryService: сервис, предоставляющий информацию о запущенных и завершенных инстансах.

  • AuthorizationService: управление доступами.

В наших проектах Camunda была интегрирована со SpringBoot, поэтому при поднятии SpringBoot приложения создавали все необходимые для работы Camunda классы: и ProcessEngine, и другие сервисы Camunda. 

Поэтому первый подход очевидный – запускать тестовый процесс на SpringBoot. Преимущества: мы будем иметь бины всех делегатов, работающую бизнес-логику. Недостатки: такие тесты тяжелые и требуют время на поднятие контекста. 

Если мы хотим протестировать отдельный небольшой участок схемы, или у нас нет необходимости/возможности поднимать Spring-контекст, то целесообразно использовать второй подход – запуск процесса на StandaloneInmemory движке для Junit5. Так мы получим быстро работающие тесты без поднятия всего контекста Spring.

Далее рассмотрим конфигурацию в одном и другом случае: SpringBootTest и Junit5.

Запуск тестового процесса Spring Boot

Для минимальной конфигурации в случае со Spring Boot достаточно поставить аннотацию @SpringBootTest, и мы получаем внедренную зависимость ProcessEngine с сервисами. Базу данных для Camunda в тестах можно развернуть в тестконтейнере или h2 inMemory.

spring:
 profiles: bpm-process-tests
 datasource:
   url: jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE
   username: admin
   password: p
   driverClassName: org.h2.Driver
camunda:
 bpm:
   admin-user:
     id: demo
     password: demo
     firstName: Demo
   filter:
     create: All tasks

Старт инстанса процесса осуществляется через вызов runtimeService.startProcessInstanceByKey.

import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
@ActiveProfiles("bpm-process-tests")
public class SpringBootProcessTest {

    final String PROCESS_KEY = "PROCESS_KEY";

    Map<String, Object> variables;

    @Autowired
    RuntimeService runtimeService;

    @Test
    public void test() {
        variables = new HashMap<>();
        variables.put("CLIENT_ID_KEY", "test_client_id");

        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(PROCESS_KEY, variables);

        assertNotNull(processInstance);
    }
}

Запуск тестов с контекстом Spring полезен, если вы используете АОП. Проблему согласованности контекста переменных Camunda можно решить так: в каждом делегате определить метод getRequiredVariables, который будет возвращать список необходимых делегату переменных, а аспект Spring перед вызовом метода execute() делегата будет вызывать метод getRequiredVariables() и проверять контекст Camunda на наличие этих переменных.

Запуск тестового процесса на Junit5

Другой подход заключается в запуске движка Camunda в качестве SatandaloneInMemory для Junit5. В этом случае необходимо подключить к проекту библиотеку:

<dependency>
   <groupId>org.camunda.bpm.extension</groupId>
   <artifactId>camunda-bpm-junit5</artifactId>
   <version>1.0.2</version>
   <scope>test</scope>
</dependency>

Далее нужно сконфигурировать ProcessEngine файлом /resources/camunda.cfg.xml. Указываем настройки базы данных, активируем HistoryService, подключаем spin-plugin для сериализации / десериализации объектов.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">

   <bean id="processEngineConfiguration" class="org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">

       <property name="jdbcUrl" value="jdbc:h2:mem:camunda;DB_CLOSE_DELAY=1000" />
       <property name="jdbcDriver" value="org.h2.Driver" />
       <property name="jdbcUsername" value="sa" />
       <property name="jdbcPassword" value="" />
       <property name="databaseSchemaUpdate" value="true" />

       <!-- job executor configurations -->
       <property name="jobExecutorActivate" value="true" />

       <property name="history" value="full" />
       <property name="expressionManager">
           <bean class="org.camunda.bpm.engine.test.mock.MockExpressionManager"/>
       </property>

       <property name="processEnginePlugins">
         <list>
           <bean class="org.camunda.spin.plugin.impl.SpinProcessEnginePlugin" />
         </list>
       </property>
   </bean>
</beans>

В аннотации к тестовому классу указываем расширение для Junit @ExtendWith(ProcessEngineExtension.class), которое предоставляет библиотека. В этом случае мы также получаем ProcessEngine и можем стартовать инстанс процесса, вызвав runtimeService.

import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.test.Deployment;
import org.camunda.bpm.extension.junit5.test.ProcessEngineExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@ExtendWith(ProcessEngineExtension.class)
@Deployment(resources = "loanApproval.bpmn")
public class JunitProcessTest {

   public ProcessEngine processEngine;

   final String PROCESS_KEY = "PROCESS_KEY";

   Map<String, Object> variables;

   @Test
   public void test() {
       variables = new HashMap<>();
       variables.put("CLIENT_ID_KEY", "test_client_id");

       ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey(PROCESS_KEY, variables);

       assertNotNull(processInstance);
   }
}

Если в случае с ProcessEngine на SpringBoot мы имеем рабочую логику и созданные бины делегатов, то при использовании StandaloneInMemory ProcessEngine на Junit все делегаты нужно мокать, т.к. java-делегаты с зависимостями не будут работать. Есть два варианта привязки java-делегата к сервис-таскам на схеме: через наименование класса и через Delegate expression. Во втором случае указывается название бина.

Есть библиотека, которая может замокать сразу все Delegate Expression на схеме при помощи статического метода autoMock().

<dependency>
   <groupId>org.camunda.bpm.extension.mockito</groupId>
   <artifactId>camunda-bpm-mockito</artifactId>
   <version>5.15.0</version>
   <scope>test</scope>
</dependency>
import static org.camunda.bpm.extension.mockito.DelegateExpressions.autoMock;
...
public class JunitProcessTest {
   	autoMock("loanApproval.bpmn");
...

Необходимо учитывать: мокать библиотека может только Delegate expression, а не классы. При проектировании процесса в моделлере на это стоит обратить внимание и связывать сервис-таски с java-кодом через Delegate expression и названия бинов, а не полное наименование классов. Иногда количество делегатов доходит до нескольких десятков, поэтому autoMock в этом случае – удобная функция. Моки delegate expression работают корректно, только если в тестах отключен job-executor. А зачем его отключать, подробнее рассмотрим чуть ниже.

Ассерты

Итак, мы имеем возможность запускать процесс в тестовой среде. Как нам посмотреть и сделать утверждение, что процесс прошел нужные activity? 

ProcessEngine предоставляет нам сервис HistoryService. Обратившись к нему, можно получить список activity, переменных из контекста и тд. Имея идентификатор запущенного инстанса процесса, можно извлечь список завершенных activity. Далее, взяв идентификатор activityId со схемы, можно создать ассерт о том, что нужная activity находится в списке выполненных активностей. Аналогично можно ассертить, что переменная не null, или, например, приводить ее к нужному типу и сравнивать значение.

…
List<HistoricActivityInstance> activities = processEngine.getHistoryService().createHistoricActivityInstanceQuery()
       .processInstanceId(processInstance.getProcessInstanceId())
       .finished()
       .orderByHistoricActivityInstanceEndTime().asc()
       .orderPartiallyByOccurrence().asc()
       .list();
final List<String> finishedActivityIds = activities.stream().map(HistoricActivityInstance::getActivityId)
       .collect(Collectors.toUnmodifiableList());

assertThat(finishedActivityIds).contains("Activity_0gqoxk1");

List<HistoricVariableInstance> variables = processEngine.getHistoryService().createHistoricVariableInstanceQuery()
       .processInstanceId(processInstance.getProcessInstanceId())
       .list();
final List<String> variableNames = variables.stream().map(HistoricVariableInstance::getName)
       .collect(Collectors.toUnmodifiableList());

assertThat(variableNames).contains("CLIENT_ID");

HistoricVariableInstance variable = processEngine.getHistoryService().createHistoricVariableInstanceQuery()
       .processInstanceId(processInstance.getProcessInstanceId())
       .variableName("CLIENT_ID")
       .singleResult();

assertThat(variable.getValue()).isEqualTo("test_value");
…

Вызывать списки activity и переменных таким образом не очень удобно, поэтому по best practice рекомендуется использовать дополнительные инструменты. Очень полезной здесь будет библиотека camunda-bpm-assert.

<dependency>
   <groupId>org.camunda.bpm.assert</groupId>
   <artifactId>camunda-bpm-assert</artifactId>
   <version>13.0.0</version>
   <scope>test</scope>
</dependency>

Библиотека предоставляет ассерт-методы и статические методы для получения сервисов движка. С ее помощью ассертить пройденные таски, порядок прохождения, переменные и тд. Под капотом она также работает через HistoryService, ManagementService и предоставляет статические методы для удобного получения этих сервисов. Не стоит путать методы assertThat этой библиотеки с методами другой часто используемой библиотеки Jupiter.Assertions.AssertThat. 

//процесс
BpmnAwareTests.assertThat(processInstance).isStarted();
BpmnAwareTests.assertThat(processInstance).isEnded();

//активити
BpmnAwareTests.assertThat(processInstance).isWaitingAt("Activity_0o6fb6r");
BpmnAwareTests.assertThat(processInstance).hasPassed("Activity_0gqoxk1");
BpmnAwareTests.assertThat(processInstance).hasPassedInOrder("Activity_0gqoxk1, Activity_0o6fb6r");
BpmnAwareTests.assertThat(processInstance).hasNotPassed("Activity_0o6fb6r");

//переменные
BpmnAwareTests.assertThat(processInstance).hasVariables("CLIENT_ID", "CHECK_RESULT");


//сервисы движка
BpmnAwareTests.runtimeService();
BpmnAwareTests.managementService();
BpmnAwareTests.historyService();
BpmnAwareTests.processEngine();

Итак, в тестовой среде у нас есть сконфигурированный ProcessEngine, сервисы Camunda. Мы имеем возможность запускать тестовый процесс и создавать нужные утверждения. Но проблемы написания тестов возникают из-за особенностей Camunda – асинхронности и транзакций в выполнении процесса.

Асинхронность Camunda

При работе со схемами Camunda важно учитывать то, как Camunda управляет процессом. Она разбивает процесс на участки, которые можно выполнять асинхронно, транзакционно, в разных потоках. Даже если мы имеем две последовательные транзакции, Camunda делает это в разных потоках. Asynchronous continuations – это break-point’ы в выполнении процесса, которые используются для привязки транзакций. Подробнее можно почитать в документации Camunda. Сейчас отметим только, что некоторые activity имеют свойство async-before (указывается в моделере).

Это означает, что перед выполнением activity, инстанс процесса персистится в базу данных Camunda, транзакция коммитится и начинается следующая. В случае ошибки транзакция откатывается. 

Движок Camunda создает под них джобы и управляется это job-executer’ом. Джобы, например, также создаются, когда есть привязанные таймеры. При написании тестов можно столкнуться с ситуацией, что тест запускается и завершается, но процесс проходит путь только до первой таски с async-before. Дело в том, что вместе с завершением выполнения джобы завершается и поток, и тест. Чтобы процесс шел дальше, нельзя давать потоку теста завершаться. Выход – задать ожидание выполнения процесса в тесте. Хорошим вариантом для реализации метода может быть применение awaitility библиотеки.

<dependency>
   <groupId>org.awaitility</groupId>
   <artifactId>awaitility</artifactId>
   <version>4.1.1</version>
   <scope>test</scope>
</dependency>

Вызывая HistoryService и зная идентификатор инстанса процесса и идентификатор activity, не даем тесту завершатся, пока нужная activity не будет пройдена.

private static final ConditionFactory WAIT = await()
       .atMost(Duration.ofSeconds(10))
       .pollInterval(Duration.ofMillis(500))
       .pollDelay(Duration.ofMillis(1));

protected void waitUntilActivityWillBeExecuted(String processId, String activityId) {
   WAIT.untilAsserted(() -> {
       boolean activityIsFinished = processEngine.getHistoryService().createHistoricActivityInstanceQuery()
               .processInstanceId(processId)
               .finished()
               .list()
               .stream()
               .anyMatch(activity -> activity.getActivityId().equals(activityId));
       assertTrue(activityIsFinished);
   });
}

@Test
public void test() {
   ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey(PROCESS_KEY, variables);

   waitUntilActivityWillBeExecuted(processInstance.getProcessInstanceId(),"Activity_0gqoxk1");

   BpmnAwareTests.assertThat(processInstance).hasPassed("Activity_0gqoxk1");
}

Такой подход имеет место, но мы решили, отказаться от ожидания в тестах.

Отключение job-executor

Чтобы отключить асинхронность Camunda, best practice предлагает отключать job-executor Camunda в юнит-тестах. Сделать это можно:

  • через конфигурацию StandaloneInMemory движка для Junit:

<!-- job executor configurations -->
<property name="jobExecutorActivate" value="true" />
  • в application properties, если ProcessEngine запускается на SpringBoot:

camunda:
 bpm:
   job-execution:
     enabled: false

Отключение job-executor приводит к тому, что каждую джобу нужно запускать вручную. С помощью camunda-bpm-assert необходимо делать ассерт, что инстанс процесса ждет на какой-то таске и вызывать execute(job()). Это еще один полезный метод библиотеки, он обращается к ManagementService и, если в очереди только одна джоба, она начнет выполняться. В случае привязанного к таске таймером двух джоб их нужно извлекать из списка.

@Test
public void test() {
   ProcessInstance processInstance = processEngine.getRuntimeService().startProcessInstanceByKey(PROCESS_KEY, variables);

   BpmnAwareTests.assertThat(processInstance).isStarted();

   BpmnAwareTests.assertThat(processInstance).isWaitingAtExactly("Activity_0o6fb6r");
   System.out.println("JOBS: " + managementService().createJobQuery().list());
   BpmnAwareTests.execute(BpmnAwareTests.job());

   BpmnAwareTests.assertThat(processInstance).isWaitingAtExactly("Activity_0gqoxk1");
   System.out.println("JOBS: " + managementService().createJobQuery().active().list());
   BpmnAwareTests.execute(BpmnAwareTests.jobQuery().processInstanceId(processInstance.getId()).list().get(0));

   BpmnAwareTests.assertThat(processInstance).hasPassed("Activity_0gqoxk1");
   BpmnAwareTests.assertThat(processInstance).isEnded();
}

Это может выглядеть странно. Взамен ожидания выполнения процесса предлагать еще более неудобное ручное выполнение. Но ручное исполнение джобов дает больший контроль над процессом, и именно в тестах это может быть полезно, особенно при тестировании ответвлений.

Camunda-platform-scenario

Чтобы упростить ручной контроль процесса и ручной запуск джобов при отключенном job-executor, Camunda-сообщество рекомендует применять библиотеку camunda-platform-scenario.

<dependency>
   <groupId>org.camunda.bpm.extension</groupId>
   <artifactId>camunda-bpm-assert-scenario</artifactId>
   <version>1.1.1</version>
   <scope>test</scope>
</dependency>

Предлагается условно делить процесс на состояния ожидания. К ним относятся, например, userTask, receiveTask, eventBasedGateway, промежуточные перехватывающие события, т.е. места, где процесс ожидает действия пользователя или ждет события, таймера и т.д.

В этих WaitStates необходимо задать поведение процесса. Библиотека предписывает создавать мок processScenario. Он будет определять поведение процесса в этих местах ожидания, но это еще не процесс-инстанс, а шаблон поведения процесса. 

Такие методы поведения мока можно выделить в абстрактный класс и потом использовать как given-пререквизит при написании тестов, задавать поведение процесса. В github репозитории библиотеки есть очень много примеров, как мокать разные activity (call-activity, intermediate activity, messageEvent и др.). 

Приведем примеры методов, которые задают поведение event-based гейтвея, ожидающего получение некоторого сообщения. Зададим 2 случая – сообщение получено с переменной контекста или сообщение не получено, в случае чего на гейтвее сработает таймер:

import org.camunda.bpm.scenario.ProcessScenario;
import org.camunda.bpm.scenario.act.EventBasedGatewayAction;
import org.camunda.bpm.scenario.delegate.EventBasedGatewayDelegate;
import org.mockito.Mock;
import org.mockito.Mockito;

import java.util.HashMap;
import java.util.Map;

public abstract class AbstractProcessTest {

   @Mock
   ProcessScenario processScenario;

   /**
    * Стабирует наступление ожидаемого события на event-based гейтвее 'Gateway_0podfat'
    */
   protected void checkClient_gateway_eventTrigger(Boolean checkResult) {
       Mockito.when(processScenario.waitsAtEventBasedGateway("Gateway_0podfat"))
               .thenReturn(new EventBasedGatewayAction() {
                   @Override
                   public void execute(EventBasedGatewayDelegate gateway) throws Exception {
                       Map<String, Object> vars = new HashMap<>();
                       vars.put("CLIENT_CHECK", checkResult);
                       gateway.getEventSubscription("Event");
                   }
               });
   }

   /**
    * Стабирует срабатывание таймера на event-based гейтвее 'Gateway_0podfat'
    */
   protected void checkClient_gateway_timerTrigger() {
       Mockito.when(processScenario.waitsAtEventBasedGateway("Gateway_0podfat"))
               .thenReturn(new EventBasedGatewayAction() {
                   @Override
                   public void execute(EventBasedGatewayDelegate gateway) throws Exception {
                       //Do nothing to trigger the timer
                   }
               });
   }
}

Далее запускаем процесс, используя processScenario. Стартовать в этом случае следует через Scenario, а не через runtimeService, как делалось ранее. Задаем нужные ассерты и получаем given-when-then тесты. Поскольку все методы из пререквизита вынесены в абстрактный класс, то если нужно будет поменять сценарий, заменяем метод в пререквизите на другой и делаем другие ассерты. Таким способом можно быстро создавать новые тесты.

public class ProcessScenarioTest extends AbstractProcessTest {

   final String PROCESS_KEY = "PROCESS_KEY";

   @Test
   public void test() {
       checkClient_gateway_eventTrigger(true);
       createOrder_gateway_timerTrigger();
       confirmOrder_message_eventTrigger();

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

       BpmnAwareTests.assertThat(handler.instance(processScenario)).isStarted();
       verify(processScenario).hasCompleted("Activity_0qdw0q8");
       verify(processScenario).hasCompleted("Activity_16wankn");
       BpmnAwareTests.assertThat(handler.instance(processScenario)).isEnded();
   }
}

Если требуется запустить процесс с определенной activity, такая возможность тоже имеется:

…
Scenario handler = Scenario.run(processScenario).startBy(new ProcessStarter() {
   @Override
   public ProcessInstance start() {
       Map<String, Object> variables = new HashMap<>();
       variables.put("CLIENT_ID", "1-2ASDF");
       variables.put("CLIENT_CHECK", true);

       return BpmnAwareTests.runtimeService().createProcessInstanceByKey(PROCESS_KEY)
               .setVariables(variables)
               .startBeforeActivity("Activity_0qdw0q8")
               .execute();
   }
})
       .execute();
…

Camunda-platform-scenario может использоваться как с движком Camunda на Junit, так и с движком в SpringBootTest.

Визуализация тестов

Существует еще одна полезная библиотека – Process-test-coverag. Она позволяет визуализировать покрытие модели тестами.

<dependency>
   <groupId>org.camunda.bpm.extension</groupId>
   <artifactId>camunda-bpm-process-test-coverage-core</artifactId>
   <version>1.0.0</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.camunda.bpm.extension</groupId>
   <artifactId>camunda-bpm-process-test-coverage-junit5</artifactId>
   <version>1.0.0</version>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.camunda.bpm.extension</groupId>
   <artifactId>camunda-bpm-process-test-coverage-spring-test</artifactId>
   <version>1.0.0</version>
   <scope>test</scope>
</dependency>

Библиотека формирует отчет и отображает тест-сьюты, модель процесса, окрашивая пройденные участки в зеленый цвет. Если тест упадет, можно увидеть, в какую сторону ушел процесс. Так будет проще сделать вывод о том, что пошло не так. 

Библиотека работает и на ProcessEngine Junit, и на ProcessEngine со SpringBoot. Конфигурация – предельно простая. В случае с Junit мы меняем extension:

@ExtendWith(ProcessEngineCoverageExtension.class)
public class MyProcessTest

Далее немного корректируем конфиг-файл движка:

<bean id="processEngineConfiguration"
   class="org.camunda.bpm.extension.process_test_coverage.engine.ProcessCoverageInMemProcessEngineConfiguration">
   ...
</bean>

В случае со SpringBoot подключаем TestExecutionListeners и CoverageTestConfiguration из библиотеки.

@SpringBootTest
@Import({CoverageTestConfiguration.class, ProcessEngineCoverageConfiguration.class})
@TestExecutionListeners(value = ProcessEngineCoverageTestExecutionListener.class,
        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
public class OrderProcessTest {

На момент написания статьи версия библиотеки для Spring подтягивается некорректно, а классы ProcessEngineCoverageConfiguration.class, ProcessEngineCoverageTestExecutionListener.class отсутствуют. Однако их можно взять из исходников на github и поместить в свой проект.

После выполнения тестов формируются html отчеты и сохраняются в отдельный каталог в проекте. В них также показывается в процентах доля задействованных activity:

Мы рассказали о своем опыте и возможностях тестирования процессов Camunda. Как вы могли увидеть, здесь есть варианты способов запуска движка Camunda в тестах – StandaloneInMemory на Junit или ProcessEngine со Spring Boot, и исполнения джоба – со включенным или отключенным job-executor. 

Каждая команда разработчиков может выбирать, насколько приближенный к реальному процесс хочет иметь в тестах, и комбинировать подходы к тестированию схемы. Это позволит покрывать процесс Given-when-then тестами, подходящими для конкретной задачи, задавать ассерты состояния, а также фиксировать пройденные точки и генерировать удобные для восприятия отчеты.

Надеемся, наша статья была полезной! Напомним, предыдущая часть – здесь.

В завершение хотим поделиться с вами несколькими полезными ссылками, где можно почерпнуть интересную информацию по теме:

Комментарии (5)


  1. ganqqwerty
    04.04.2022 10:41
    +1

    а вот объясните, пожалуйста, кто пользуется такими тулами как камунда? Просто ж вроде если это для бизнесовых ребят, которым нужны квадратики и которые не умеют кодить - тут какие-то сложности с полноценным Спрингом, а если для джава кодеров - то нафига нужна эта схемка?


    1. SSul Автор
      04.04.2022 11:18

      Добрый день.
      В данном случае bpm предназначена для одного из российских банков. По нашим наблюдениям, Camunda бывает удобны как для бизнеса, так и для разработчиков, поскольку позволяет привязывать к схеме делегатный джава-код и сильно ускоряет процесс разработки.
      Как мы отмечали в прошлой статье, в чистом коде реализация займет гораздо больше времени, а поддерживать может быть нелегко из-за запутанной паутины вызовов и управления доступом. Хотя где-то действительно можно и проще работать без bpm – каждый решает сам.


    1. DmitriiPisarenko
      04.04.2022 12:11

      объясните, пожалуйста, кто пользуется такими тулами как камунда?

      Банки, страховки, операторы мобильной связи.

      Камунда и подобные системы нужны, когда в рамках того или иного бизнес-процесса (например, покупка клиентом мобильного телефона с сим-картой) нужно взаимодействовать со многими разными системами (например, техническая активация сим-карты, проверка платежеспособности, автоматическое определение типа тарифа -- pre-paid или post-paid и т. п.).

      Одно из преимуществ -- визуализация. Когда в процессе что-то пошло не так, можно на схеме посмотреть где и что именно. Если процесс сложный и сильно вложенный (как активация сим-карты) это очень помогает.


    1. ultrinfaern
      04.04.2022 14:24

      Если у вас сложный бизнес процесс, и там НУЖНО ручное участие пользователя, то такие движки - это самое то.

      Если участия пользователя нет, то я бы не стал в бпм выносить кишки обработки оно сложности не уменьшит а только увеличит, дополнительной конфигуриуемости не даст. В этом случае лучше смотреть в сторону движков конечных автоматов.


      1. vasyakolobok77
        04.04.2022 22:56

        Если у вас сложный бизнес процесс, то использование таких движков еще сильнее усложнит разработку. БПМН - это и есть описание конечного автомата, просто его можно воочию посмотреть. И для сложных БП логичней использовать акторную модель.