Привет! Меня зовут Анатолий, я ведущий разработчик в ITFB Group, и сегодня я хочу рассказать о том, как можно превратить скучные тесты в главного специалиста по продукту.

Проблема: ваши тесты — это китайская грамота для бизнеса

Представьте: ваши JUnit-тесты проходят, CI/CD зелёный, все довольны. Но тут приходит бизнес-аналитик и спрашивает: "А этот сценарий проверяет, что будет, если клиент с рейтингом 700 запросит 10 миллионов?" Вы начинаете лихорадочно копаться в коде, пытаясь найти тот самый @Test...

Знакомая картина? А что если бы ответ на этот вопрос лежал не в глубинах Java-кода, а в красивом, читаемом файле, который понятен всем — от тимлида до заказчика?

Сегодня я расскажу, как можно превратить тестирование из технической рутины в живой диалог между командами.

Что мы получаем, добавляя Cucumber и Allure?

Единый язык для команды: Сценарии на языке Gherkin становятся мостом между аналитиками, тестировщиками и разработчиками

Живая и исполняемая документация: Feature-файлы всегда актуальны, так как являются самим кодом тестов

Прозрачность и наглядность: Allure-отчеты делают результаты прогона тестов интуитивно понятными для всех участников проекта

Минимальные затраты на внедрение: Мы не меняем основной стек разработки, а лишь надстраиваем над ним удобный и стандартизированный слой BDD

Собираем наш BDD-бургер (Cucumber, JUnit, Allure)

Давайте посмотрим на наши главные ингредиенты:

 Инструмент

Роль в экосистеме

 Cucumber

Интерпретирует сценарии на языке Gherkin и связывает их с Java-кодом

 JUnit

Организует запуск тестовых сценариев

 Allure

Визуализирует результаты выполнения в виде детальных и наглядных отчетов

 AssertJ

Обеспечивает удобные и читаемые утверждения

Настройка проекта: добавляем специи к существующему стеку


Добавляем необходимые зависимости в pom.xml:

<properties>
    <cucumber.version>7.14.0</cucumber.version>
    <allure.version>2.21.0</allure.version>
</properties>

<dependencies>
    <!-- Cucumber -->
        <dependency>
            <groupId>org.camunda.bpm</groupId>
            <artifactId>camunda-engine</artifactId>
            <version>7.21.0</version>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.25.3</version>
            <scope>test</scope>
        </dependency>
        <!-- Интеграция Allure с Cucumber — формирует отчёты о прохождении тестов -->
        <dependency>
            <groupId>io.qameta.allure</groupId>
            <artifactId>allure-cucumber7-jvm</artifactId>
            <version>${allure.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Cucumber Spring — связывает Cucumber со Spring-контекстом (инжекция бинов в step definitions) -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-spring</artifactId>
            <version>7.16.1</version>
            <scope>test</scope>
        </dependency>

        <!-- Cucumber JUnit Platform Engine — интеграция Cucumber с JUnit 5 (через JUnit Platform) -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit-platform-engine</artifactId>
            <version>7.16.1</version>
            <scope>test</scope>
        </dependency>

        <!-- Cucumber Java: реализация шагов на Java для сценариев на языке Gherkin -->
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>7.16.1</version>
            <scope>test</scope>
        </dependency>
</dependencies>
<build>
 <plugins>
<!-- Maven Surefire — запуск юнит-тестов и передача параметров Allure -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <systemProperties>
                        <property>
                            <name>allure.results.directory</name>
                            <value>${project.build.directory}/allure-results</value>
                        </property>
                    </systemProperties>
                </configuration>
            </plugin>

            <!-- Плагин Allure для Maven — генерирует HTML-отчёты из результатов тестов -->
            <plugin>
                <groupId>io.qameta.allure</groupId>
                <artifactId>allure-maven</artifactId>
                <version>2.13.0</version>
                <configuration>
                    <reportVersion>${allure.version}</reportVersion>
                </configuration>
            </plugin>
</plugins>
    </build>

Структура проекта остаётся логичной:

src/test/
├── java/
│ └── com/example/tests/
│ ├── runner/
│ │ └── RunCucumberTest.java
│ └── steps/
│ └── LoanApprovalSteps.java
└── resources/
 └── features/
 └── loan-approval.feature

Бизнес-кейс: кредитная заявка

Рассмотрим типичный бизнес-кейс — систему одобрения кредитов. У нас есть простой процесс в Camunda:

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
                  xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
                  xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
                  xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
                  xmlns:camunda="http://camunda.org/schema/1.0/bpmn"
                  id="Definitions_1"
                  targetNamespace="http://camunda.org/examples"
                  exporter="Camunda Modeler"
                  exporterVersion="5.22.0">

  <bpmn:collaboration id="Collaboration_0ywjglb">
    <bpmn:participant id="Participant_0j75s7a" name="loanApproval" processRef="loanApproval" />
  </bpmn:collaboration>

  <bpmn:process id="loanApproval" name="Loan Approval Process" isExecutable="true" camunda:historyTimeToLive="180">


    <bpmn:startEvent id="StartEvent" name="Loan Application Received">
      <bpmn:outgoing>Flow_1</bpmn:outgoing>
    </bpmn:startEvent>

    <bpmn:serviceTask id="CheckCreditHistoryTask" name="Check Credit History" camunda:delegateExpression="${checkCreditHistoryDelegate}">
      <bpmn:incoming>Flow_1</bpmn:incoming>
      <bpmn:outgoing>Flow_2</bpmn:outgoing>
    </bpmn:serviceTask>

    <bpmn:userTask id="ApproveLoanTask" name="Approve Loan Application" camunda:assignee="managers">
      <bpmn:extensionElements>
        <camunda:taskListener event="create" delegateExpression="${completeTaskUserListener}" />
      </bpmn:extensionElements>
      <bpmn:incoming>Flow_2</bpmn:incoming>
      <bpmn:outgoing>Flow_3</bpmn:outgoing>
    </bpmn:userTask>

    <bpmn:endEvent id="EndEvent" name="Loan Approved">
      <bpmn:incoming>Flow_3</bpmn:incoming>
    </bpmn:endEvent>

    <bpmn:sequenceFlow id="Flow_1" sourceRef="StartEvent" targetRef="CheckCreditHistoryTask" />
    <bpmn:sequenceFlow id="Flow_2" sourceRef="CheckCreditHistoryTask" targetRef="ApproveLoanTask" />
    <bpmn:sequenceFlow id="Flow_3" sourceRef="ApproveLoanTask" targetRef="EndEvent" />

  </bpmn:process>

  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0ywjglb">
      <bpmndi:BPMNShape id="Participant_0j75s7a_di" bpmnElement="Participant_0j75s7a" isHorizontal="true">
        <dc:Bounds x="156" y="93" width="600" height="250"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="StartEventShape" bpmnElement="StartEvent">
        <dc:Bounds x="240" y="200" width="36" height="36"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="CheckCreditHistoryTaskShape" bpmnElement="CheckCreditHistoryTask">
        <dc:Bounds x="340" y="180" width="120" height="80"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="ApproveLoanTaskShape" bpmnElement="ApproveLoanTask">
        <dc:Bounds x="520" y="180" width="120" height="80"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="EndEventShape" bpmnElement="EndEvent">
        <dc:Bounds x="700" y="200" width="36" height="36"/>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
        <di:waypoint x="276" y="218"/>
        <di:waypoint x="340" y="218"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_2_di" bpmnElement="Flow_2">
        <di:waypoint x="460" y="218"/>
        <di:waypoint x="520" y="218"/>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_3_di" bpmnElement="Flow_3">
        <di:waypoint x="640" y="218"/>
        <di:waypoint x="700" y="218"/>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>

</bpmn:definitions>

Ключевые бизнес-правила (те самые, которые все понимают по-разному):

● Автоматический отказ при низком кредитном рейтинге

● Одобрение менеджера для крупных сумм даже при хорошей истории

● Автоматическое одобрение для небольших сумм надежным клиентам

Магия Gherkin: Как бизнес-правила становятся кодом (и все всё понимают)

Вот где начинается настоящая магия! Мы явно описываем бизнес-правила в сценариях.

Файл: loan-approval.feature

Реализация шагов: где магия встречается с кодом

А вот как мы связываем эти красивые сценарии с реальным кодом:

package com.example.cucumberex.cucumber;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.history.HistoricVariableInstance;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.springframework.beans.factory.annotation.Autowired;

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

import static org.assertj.core.api.Assertions.assertThat;

/*
Cucumber Step Definitions для тестирования процесса Camunda BPM.
Он связывает сценарии Cucumber (Given/When/Then) с кодом.
Через RuntimeService запускает процесс «loanApproval».
Через HistoryService проверяет результат (одобрен или отклонён кредит).
Использует аннотации @Given, @When, @Then для соответствия шагам сценария.
 */
public class LoanApprovalSteps {

    @Autowired
    private RuntimeService runtimeService;

    @Autowired
    private HistoryService historyService;

    private ProcessInstance processInstance;
    private String clientName;
    private Long loanAmount;

    @Given("a client {string} submits a loan application for {long}")
    public void clientSubmitsApplication(String clientName, Long loanAmount) {
        this.clientName = clientName;
        this.loanAmount = loanAmount;

        Map<String, Object> variables = new HashMap<>();
        variables.put("clientName", clientName);
        variables.put("loanAmount", loanAmount);

        processInstance = runtimeService.startProcessInstanceByKey("loanApproval", variables);
        assertThat(processInstance).isNotNull();
    }

    @When("the process is executed")
    public void processIsExecuted() {
        // User task will be automatically completed by TaskListener
    }

    @Then("the loan should be approved")
    public void loanShouldBeApproved() {
        assertThat(processInstance).isNotNull();

        HistoricVariableInstance loanApprovedVar =
                historyService.createHistoricVariableInstanceQuery()
                        .processInstanceId(processInstance.getId())
                        .variableName("loanApproved")
                        .singleResult();

        assertThat(loanApprovedVar).isNotNull();
        Boolean loanApproved = (Boolean) loanApprovedVar.getValue();
        assertThat(loanApproved).isTrue();
    }

    @Then("the loan should be rejected")
    public void loanShouldBeRejected() {
        assertThat(processInstance).isNotNull();

        HistoricVariableInstance loanApprovedVar =
                historyService.createHistoricVariableInstanceQuery()
                        .processInstanceId(processInstance.getId())
                        .variableName("loanApproved")
                        .singleResult();

        assertThat(loanApprovedVar).isNotNull();
        Boolean loanApproved = (Boolean) loanApprovedVar.getValue();
        assertThat(loanApproved).isFalse();
    }
}

Он создаст интерактивный отчет, который покажет:

● Какие сценарии прошли, а какие нет

● Детализацию по каждому шагу

● Значения переменных в момент теста

● Время выполнения каждого шага

Это делает процесс тестирования абсолютно прозрачным. Больше никаких "а у меня на машине работало!".

После успешного выполнения тестов запускается команда mvn allure:serve. Эта команда генерирует Allure-отчёт из полученных результатов и автоматически запускает локальный веб-сервер, а затем открывает сгенерированный index.html файл в браузере по умолчанию.

Отчёт содержит несколько ключевых страниц:

  • Overview: На главной странице отображается сводка по запуску — общее количество тестов, процент успешного выполнения и график с историей запусков.

  • Suites: Вкладка, которая отображает все тесты в виде иерархической структуры, соответствующей организации кода (пакеты, классы, методы). Она позволяет быстро найти тесты в конкретном классе или пакете, в отличие от бизнес-ориентированного представления в Behaviors. Это основной вид для анализа результатов с точки зрения структуры проекта, а не функциональности.

  • Graphs: Раздел содержит различные диаграммы, которые визуализируют результаты тестирования, распределяя тесты по статусам (passed, failed, broken и т.д.), а также анализируют их продолжительность.

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

Итоги: небольшое улучшение с большой выгодой

Итак, что мы получили в итоге? Мы не просто "настроили Cucumber". Мы подарили проекту общую нервную систему.

Единый язык: Бизнес-правила теперь явно описаны в сценариях, а не спрятаны в коде

Живая документация: loan-approval.feature — это и тест, и спецификация, которая всегда актуальна

Прозрачность процессов: Allure-отчёты делают ход теста понятным для не-технарей

Усиление регрессионного тестирования: Легко добавлять новые сценарии, не ломая старые

Минимальные затраты: Мы не переписывали тесты с нуля, а лишь надстроили над ними BDD-слой

Это не революция. Это — эволюция. Небольшая надстройка над вашим текущим стеком, которая даёт колоссальный эффект. Потратьте день на интеграцию, чтобы сэкономить десятки часов в будущем на объяснениях, поиске багов и переписывании "мёртвой" документации.

Как говорят у нас в ITFB Group: "Лучший код — это не только работающий код, но и код, который все понимают." А с BDD это понимание становится реальностью.

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