Как тестировать бизнес-процессы в Camunda: пошаговый гайд
Привет. На связи Вероника. Ранее подробно ответила на вопрос, зачем backend-разработчику Camunda. Написала про мониторинг бизнес-процессов в Camunda 8. В новой статье хочу призвать вас активно тестировать процессы в Camunda и на понятном примере показать, как это устроено. Ладно, когда у вас пара воркеров, а если это большой процесс с разветвлениями и разными вариантами развития логики? А так написал чуть больше сотни тестов и сразу на душе легче стало. Не шучу, на один из процессов я разработала их почти 150.
Ошибки в бизнес-процессах
Для того, чтобы продемонстрировать, как тестировать процессы, давайте создадим новую диаграмму, показывающую daily routine перед работой.
Что будет, если не удается проснуться, поесть или раздать корм домашним животным? Ошибка, которая ведет к завершению процесса — уведомляется PM, мол, простите-извините, работать не получится.
Если не удается выпить чашечку кофе, это печально, но не критично для процесса. Выбрасываем error boundary event, чтобы в дальнейшем было видно, что в процессе произошла внештатная ситуация, и завершаем процесс. Напоминаю, про элементы BPMN схемы можно почитать здесь.
Код вы можете увидеть в моем гите, а сейчас посмотрим этот коммит. Замечу, в примере используется 21 Java, Spring Boot версии 3.3.6, а также spring-zeebe-starter версии 8.4.12. Для тестирования необходимо подключить зависимость:
io.camunda
spring-zeebe-test
8.4.12
test
Небольшое отступление: хотела изначально взять зависимость поновее, однако в консоли начал спамить лог The request’s security level does not guarantee that the credentials will be confidential. С ходу разобраться не удалось, поэтому остановилась на версии 8.4.12, если вдруг кто сталкивался и победил, расскажите, пожалуйста, в комментариях.
Также обновила докер компоуз, чтобы вы локально могли все запустить и прогнать процесс. Внимание, elasticsearch просто так не качается, если вы понимаете о чем я.
Использование воркеров
Объяснять, что такое воркер, сервисные таски и прочее не буду. Если вы преисполнились настолько, что решили написать тесты на ваш процесс, то вы явно понимаете, как что работает. Рекомендую использовать autoComplete = false — самим управлять завершением воркера. Это позволяет придать коду большую гибкость. Мы в работе используем реактив, и в зависимости от того, возвращается next signal или error signal, выполняем различные действия.
Что происходит в статическом методе sendZeebeCompleteCommand, можно увидеть в примере кода, приведенном ниже. Полный класс с вспомогательными методами можно посмотреть в гите.
public static void sendZeebeCompleteCommand(final JobClient client, long jobKey, ZeebeVariable variable) {
client.newCompleteCommand(jobKey)
.variables(variable)
.send()
.exceptionally(t -> {
log.error(ERROR_EXECUTE_COMMAND, t.getMessage());
throw new ZeebeBpmnError(COMMAND_EXECUTION_EXCEPTION_CODE, COMPLETE_COMMAND_EXCEPTION_MESSAGE, null);
});
}
В учебном проекте используются методы сервиса GodService, которые вызываются в воркерах.
Предположим, для PM важно знать, на каком шаге произошла критическая ошибка. Для этого введены специальные коды. Например, если не удалось проснуться, то будет код 1.
MessageSender — вспомогательный класс, который позволяет избежать дублирования кода, ведь по сути в трех воркерах при наступлении критической ошибки происходит одна и та же логика. Мы сохраняем variables в ApologyMessageData и выбрасываем Error Start Event, которое является триггером для выполнения воркера «Отправить сообщение руководителю проекта».
И уже непосредственно в воркере «Отправить сообщение руководителю проекта» можно увидеть, какой apologyCode пришел.
Приступим к тестированию процессов
Над классом с тестами необходимо навесить две аннотации, а также заавтовайрить ZeebeClient. Далее мокаем все сервисы, которые используются в воркерах. Напоминаю, задача состоит в том, чтобы протестировать процесс. То что, происходит в сервисах, остается в сервисах, для этого используются другие тесты.
Так как у нас только один божественный сервис, то, естественно, мокаем только его. В константах используются айдишники элементов, напоминаю, где их можно посмотреть.
У элемента с типом enjoyCupOfCoffeeTask — ID Activity_08×2kch, а у error boundary event — ID Event_077d3l.
Далее перед запуском каждого теста необходимо запустить все моки.
@BeforeEach
void prepareMocks() {
when(godService.hasPet()).thenReturn(true);
doNothing().when(godService).getUp();
doNothing().when(godService).haveBreakfast();
doNothing().when(godService).feedPet();
doNothing().when(godService).enjoyCupOfCoffeeTask();
}
То есть описывается поведение всех методов, которые вызываются в классах-воркерах.
Начнем с happy path. Мы проснулись, поели, покормили питомца и выпили чашечку кофе. Следовательно, элемент с отправкой сообщения PM не отработал.
Ниже приведу код методов runProcessInstance () и waitForPassingElement (long processInstanceKey, String elementId)
private ProcessInstanceEvent runProcessInstance() {
return client.newCreateInstanceCommand()
.bpmnProcessId(PROCESS_NAME)
.latestVersion()
.send()
.join();
}
private void waitForPassingElement(long processInstanceKey, String elementId) {
// ждём выполнения воркера 60 сек
waitForProcessInstanceHasPassedElement(processInstanceKey, elementId, Duration.ofSeconds(60));
}
Поясняю, что происходит в методе теста
Сначала мы ждем выполнения элементов BPMN схемы:
waitForPassingElement(processInstanceKey, GET_UP_TASK);
waitForPassingElement(processInstanceKey, HAVE_BREAKFAST_TASK);
waitForPassingElement(processInstanceKey, FEED_PET_TASK);
waitForPassingElement(processInstanceKey, ENJOY_CUP_OF_COFFEE_TASK);
Затем смотрим, что у нас есть отработавшие элементы схемы, да еще и в нужном порядке.
.hasPassedElementsInOrder(
GET_UP_TASK,
HAVE_BREAKFAST_TASK,
FEED_PET_TASK,
ENJOY_CUP_OF_COFFEE_TASK
)
Также с помощью теста мы проверяем, что процесс стартовал:
.isStarted ()
В нем не было инцидентов:
.hasNoIncidents ()
И он успешно завершен:
.isCompleted ();
Еще необходимо проверить, что не отработали элементы error boundary event, error start event и service task с типом sendMessageToProjectManagerTask:
.hasNotPassedElement(ZEEBE_ERROR_ENJOY_CUP_OF_COFFEE)
.hasNotPassedElement(ZEEBE_ERROR_SEND_MESSAGE_TO_PM_TASK)
.hasNotPassedElement(SEND_MESSAGE_TO_PM_TASK)
Так как в процессе отработал элемент «Покормить домашнее животное», значит переменная hasPet имела значение true. Это также проверяется в методе:
.hasVariableWithValue (VARIABLE_HAS_PET, true)
Вишенка на торте — проверка, что методы сервиса действительно отработали:
assertAll(
() -> verify(godService, times(1)).getUp(),
() -> verify(godService, times(1)).haveBreakfast(),
() -> verify(godService, times(1)).feedPet(),
() -> verify(godService, times(1)).enjoyCupOfCoffeeTask()
);
Теперь давайте протестируем вариант, который предусматривает, что произошла ошибка во время выполнения элемента «Поесть». Как мы помним, процесс прерывает нормальное выполнение, отрабатывают элементы error start even и service task с типом sendMessageToProjectManagerTask.
Чтобы элемент «Поесть» не отработал, надо выбросить ошибку во время выполнения метода haveBreakfast (). Если подниметесь в начало статьи, то можно ещё раз для наглядности посмотреть код воркера haveBreakfastTask.
Итак, снова ждем выполнения нужных элементов:
waitForPassingElement(processInstanceKey, GET_UP_TASK);
waitForPassingElement(processInstanceKey, ZEEBE_ERROR_SEND_MESSAGE_TO_PM_TASK);
waitForPassingElement(processInstanceKey, SEND_MESSAGE_TO_PM_TASK);
Проверяем, что запланированные элементы действительно отработали:
.hasPassedElement(GET_UP_TASK)
.hasPassedElement(ZEEBE_ERROR_SEND_MESSAGE_TO_PM_TASK)
.hasPassedElement(SEND_MESSAGE_TO_PM_TASK)
Обратите внимание, элемент «Поесть» НЕ отработал, то есть он начал свою работу — стал ACTIVATED, но в статус COMPLETED не перешел. Соответственно, также не отработали элементы «Покормить домашнее животное» и «Выпить чашечку кофе»:
.hasNotPassedElement(HAVE_BREAKFAST_TASK)
.hasNotPassedElement(FEED_PET_TASK)
.hasNotPassedElement(ENJOY_CUP_OF_COFFEE_TASK)
.hasNotPassedElement(ZEEBE_ERROR_ENJOY_CUP_OF_COFFEE)
Процесс стартовал:
.isStarted ()
В нем не было инцидентов:
.hasNoIncidents ()
И он успешно завершен:
.isCompleted ();
Проверяем значение переменной apologyCode:
.hasVariableWithValue(VARIABLE_APOLOGY_CODE,
ApologyCode.COULD_NOT_HAVE_BREAKFAST.getCode())
И куда без проверки срабатывания методов сервиса:
assertAll(
() -> verify(godService, times(1)).getUp(),
() -> verify(godService, times(1)).haveBreakfast(),
() -> verify(godService, never()).feedPet(),
() -> verify(godService, never()).enjoyCupOfCoffeeTask()
);
Также примеры тестов можно посмотреть в гите комьюнити камунды. У меня все. Ещё раз призываю писать тесты на процессы, чтобы потом не было вот так.
Всем спасибо. Пишите свои мысли в комментариях — обсудим.