Нотации BPMN (Buisness Process Model and Notation) все чаще используются для описания бизнес-процессов какой-либо предметной области реального бизнеса. В результате получается нечто среднее между блок-схемой и функциональной схемой, в которой есть:
элементы, описывающие некоторую функциональность бизнеса,
связи между элементами.
Такую схему можно реализовать в программном коде. И отсюда вытекает вопрос — как же проверить, что ПО корректно работает с составленной бизнес-моделью, когда программный код уже написан.
Привет! Я Мария, SDET-специалист IT-компании SimbirSoft. В этой статье я хочу поделиться успешным опытом тестирования процесса на основе BPMN-схемы Camunda.
Краткий обзор некоторых элементов нотации
Для лучшего понимания нотации перечислим некоторые элементы, которые могут использоваться в схеме. Пример простейшей схемы взят из открытых источников (ссылка на гит хаб https://github.com/sourabhparsekar/camunda-masala-noodles) и представлен на рис.1. В этом простом бизнес-процессе присутствуют следующие элементы:
В данном примере реализован следующий бизнес-процесс:
На старте процесса в элементе «Do I have all ingridients» формируется заявка, в которой передаются входные данные (ингридиенты, количество).
Затем в exclusive gatevay «Can we cook?» проверяется, все ли необходимое для приготовления присутствует.
Если да, переходим к элементу «Let’s cook» и начинаем готовить, если нет — переводим заказ в онлайн в элементе «Order Online».
Дожидаемся готового состояния в event based gateway «Is it ready».
От программы приходит сообщение, что готово в message intermediate catch event «It’s cooked».
Переходим к элементу «Let’s eat».
Если не дождались сообщения, срабатывает таймер и заказ переводится в онлайн «Order Online».
Процесс завершен.
Открыть схему для просмотра и редакции можно в приложении Camunda Modeler. Там же в моделере можно узнать основную информацию об элементе схемы – его ID, тип, имя, имплементация в коде, входные/выходные параметры и т.д. (рис.2).
Почему возникла необходимость тестировать бизнес-схему
Поскольку в проекте использовалась Camunda в интеграции со Spring Boot, то встал вопрос о том, как тестировать микросервис, чтобы иметь наиболее полную картину о его корректной или некорректной работе. Как уже было сказано выше, необходимо проверять идет ли процесс по нужному нам пути в том или ином кейсе, а также интеграции с внешними сервисами. Поэтому команда приняла стратегическое решение разрабатывать тесты двух видов – интеграционные и процесса в изоляции. Стоит отметить, что и те и другие реализовывали end-to-end сценарии, то есть от старта процесса до его завершения по тому или иному пути.
Ручное тестирование схемы
Для ручного тестирования схем Camunda предоставляет приложение Cockpit. Оно позволяет запускать процессы, просматривать состояние активных процессов, просматривать инциденты. Но у него есть ряд недостатков: оно не отображает историю завершенных процессов и нет возможности для управления токеном процесса.
Альтернативой Cockpit является Ex-cam-ad. В нем можно просматривать историю процессов, управлять токеном, просматривать инциденты. Ex-cam-ad также показывает состояние процесса в реальном времени.
Чтобы протестировать схему вручную, нужно:
инициировать процесс стартовым запросом, который в случае успешном запуске возвращает ID процесса,
использовать полученное ID в ex-cam-ad, где можно отследить движение токена по схеме в конкретном процессе, и, в случае падения, просматривать в логах стек трейс.
Такой подход занимает достаточно много времени, к тому же в ex-cam-ad неочевидно, если ошибка возникла в ходе реализации.
Интеграционное тестирование
Интеграционные тесты в нашем случае представляли собой end-to-end тесты. Процесс стартовал после выполнения инициализирующего запроса в RestAssured. Затем запросы отправлялись во внешние сервисы уже из самого приложения, а результаты выполнения проверялись в базе данных.
Интеграционные тесты сильно зависят от доступности внешних сервисов. И еще интеграционные тесты не показывают, что программная реализация схемы корректно работает с бизнес-моделью.
Тестирование бизнес-процесса в изоляции
Тестирование бизнес-процесса в изоляции относится к методу белого ящика, т.к. необходимо поднимать спринг-контекст приложения (аннотация @SpringBootTest, которая поднимает контекст приложения) и иметь доступ к коду. Поэтому будем создавать тесты процесса в изоляции в тестовом пространстве проекта. Несмотря на то, что такие тесты имплементируются так же, как и модульные тесты, нужно отделять их от модульных и не запускать их вместе. Это можно делать настройкой отдельного профиля (@ActiveProfiles).
/**
* Abstract base test class.
*/
@SpringBootTest
@ActiveProfiles("bpm-process-test")
public class AbstractProcessTest {
/**
* Интерфейс для освобождения ресурсов.
*/
private AutoCloseable closeable;
/**
* Мокаем сценарий через api плаформы Camunda.
*/
@Mock
protected ProcessScenario processScenario;
Зависимости, которые нам понадобятся для тестирования (тестировать будем платформу Camunda 7):
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
Мокаем ProcessScenario (из библиотеки camunda-bpm-assert-scenario). Его имплементация позволит нам определить, что должно происходить во время выполнения элементов нашей бизнес-схемы (userTask, receiveTask, eventBasedGateway – так называемые WaitStates).
Запускаем инстанс процесса по ключу, передав также в перегруженный метод переменные процесса.
Scenario handler = Scenario.run(processScenario)
.startByKey(PROCESS_KEY, variables)
.execute();
Особенности написания стабов
Все методы взаимодействия с WaitStates схемы пропишем в отдельном классе.
Метод, который стабирует делегат, на вход принимает делегат и переменные, которые делегат должен положить в контекст процесса. Интерфейс JavaDelegate содержит метод execute, куда нужно передать 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;
}
Процесс ожидает событие на гейтвее, поэтому нужен метод, который будет стабировать его. ProcessScenario содержит методы, которые должны обрабатывают события Camunda.
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);
В нашем случае нужно застабировать gateway, который ожидает наступление события It’s coocked по одному пути и срабатывание тайм аута по другому пути процесса.
/**
* Стабирует событие на гейтвее.
* @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;
}
Тесты
Перейдем к написанию тестов. В нашем примере на рис.1 три возможных пути: happy path и два пути, когда выполняется order online.
Пропишем SpyBean для делегатов.
/**
* Внедряем нужные нам зависимости делегатов спрингового приложения.
*/
@SpyBean
protected CheckIngredients checkIngredients;
@SpyBean
protected LetUsCook letUsCook;
@SpyBeanp
rotected LetUsEat letUsEat;
@SpyBean
protected OrderOnline orderOnline;
Каждый делегат имеет метод execute, в котором выполняется какая-то бизнес логика и что-то кладется в контекст приложения.
@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);
В рамках стабирования бизнес-логика нас не интересует, нам нужно передать в метод stubDelegate соответствующие переменные.
@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()
);
В блоке assertAll перечислены проверки, что процесс стартовал и завершился, а также что каждый делегат перешел в состояние hasCompleted.
Результат выполнения тестов сохраняется в виде отчета ниже с визуализацией пройденного пути и процента покрытия схемы.
Подведем итоги
В этой статье я рассмотрела довольно простую схему. На практике бизнес-процессы гораздо сложнее и содержат множество различных элементов. Представленный подход к тестированию Camunda не единственный из возможных. При этом его можно рекомендовать в качестве системного и приемочного тестирования схем bpmn на этом движке. Также тесты процесса в изоляции позволяют понять, верно ли идет процесс в том или ином кейсе и что программная реализация схемы корректно работает с бизнес-моделью.
Полезные ссылки
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
https://github.com/matteobaccan/owner
Спасибо за внимание!
Больше авторских материалов для SDET-специалистов от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.
Комментарии (3)
kain64b
08.10.2024 11:17На реальных схемах событие тупо не прийдёт так как не так обработали сообщение из очереди.. либо в gateway не заполняет параметр так как в логике парсинга ответа от сервиса ошибка.. сама схема часто статична, нет особых изменений после имплементации, то есть прогнали ветки на симуляции и забыли, до новой версии процесса. Интересно, а если вариант : покрытие юнит тестами делегатов, , все сценариев в делегатам плюсом интеграционные тесты, через тот же testcomplete /katalon автоматизацию. Плюсом собирать статистику по схеме. После прогона тестов анализ какие сценарии в интеграционных тестах забыли.
sshmakov
Если я правильно понял, ServiceTask в тесте не вызывают внешние сервисы, а замокированы, и получают констатный ответ. То есть собственно работа с rest-сервисом не проверяется - если в модели запрос неправильно написан и внешний сервис не сможет его распознать, то вы узнаете это только на интеграционном тестировании.
Поэтому предлагаю еще один вариант - сделать на Wiremock заглушки, изображающие внешние сервисы, можно даже statefull.
SSul Автор
Спасибо за замечание! На самом деле мы изначально хотели тестировать без внешних вызовов. Но вы правы — оба варианта могут быть использованы)