«1. Обзор

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

Кроме того, чтобы сделать статью автономной и независимой от каких-либо внешних REST-сервисов, мы будем использовать WireMock, библиотеку веб-сервисов-заглушек и имитаторов. Если вы хотите узнать больше об этой библиотеке, обратитесь к введению в WireMock.

2. Огурец — язык Cucumber

Cucumber — это среда тестирования, поддерживающая разработку, управляемую поведением (BDD), позволяющая пользователям определять операции приложения в виде обычного текста. Он работает на основе доменного языка Gherkin (DSL). Этот простой, но мощный синтаксис Gherkin позволяет разработчикам и тестировщикам писать сложные тесты, делая его понятным даже для нетехнических пользователей.

2.1. Введение в Gherkin

Gherkin — это построчный язык, использующий окончания строк, отступы и ключевые слова для определения документов. Каждая непустая строка обычно начинается с ключевого слова Gherkin, за которым следует произвольный текст, обычно являющийся описанием ключевого слова.

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

Вот простой пример документа Gherkin:

Feature: A short description of the desired functionality

  Scenario: A business situation
    Given a precondition
    And another precondition
    When an event happens
    And another event happens too
    Then a testable outcome is achieved
    And something else is also completed

В следующих разделах мы опишем несколько наиболее важных элементов в структуре Gherkin.

2.2. Feature

Мы используем файл Gherkin для описания функции приложения, которую необходимо протестировать. Файл содержит ключевое слово Feature в самом начале, за которым следует имя функции в той же строке и необязательное описание, которое может занимать несколько строк ниже.

Парсер Cucumber пропускает весь текст, кроме ключевого слова Feature, и включает его только в целях документации.

2.3. Сценарии и шаги

Структура корнишона может состоять из одного или нескольких сценариев, распознаваемых по ключевому слову Scenario. Сценарий — это, по сути, тест, позволяющий пользователям проверить возможности приложения. Он должен описывать исходный контекст, события, которые могут произойти, и ожидаемые результаты, создаваемые этими событиями.

Эти действия выполняются с помощью шагов, определяемых одним из пяти ключевых слов: Дано, Когда, Тогда, И и Но.

    Дано: этот шаг должен привести систему в четко определенное состояние, прежде чем пользователи начнут взаимодействовать с приложением. Предложение Given может рассматриваться как предварительное условие для варианта использования. Когда: Шаг Когда используется для описания события, которое происходит с приложением. Это может быть действие, предпринятое пользователями, или событие, инициированное другой системой. Затем: Этот шаг должен указать ожидаемый результат теста. Результат должен быть связан с бизнес-ценностями тестируемой функции. И и Но: эти ключевые слова можно использовать для замены приведенных выше ключевых слов шага, когда имеется несколько шагов одного типа.

Cucumber на самом деле не различает эти ключевые слова, однако они все еще присутствуют, чтобы сделать функцию более читаемой и совместимой со структурой BDD.

3. Реализация Cucumber-JVM

Cucumber изначально был написан на Ruby и был портирован на Java с реализацией Cucumber-JVM, которая является предметом этого раздела.

3.1. Зависимости Maven

Чтобы использовать Cucumber-JVM в проекте Maven, в POM необходимо включить следующую зависимость:

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>6.8.0</version>
    <scope>test</scope>
</dependency>

Чтобы облегчить тестирование JUnit с Cucumber, нам нужна еще одна зависимость. :

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>6.8.0</version>
</dependency>

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

3.2. Определения шагов

Сценарии огурцов были бы бесполезны, если бы они не были переведены в действия, и именно здесь в игру вступают определения шагов. По сути, определение шага — это аннотированный метод Java с прикрепленным шаблоном, задачей которого является преобразование шагов Gherkin из простого текста в исполняемый код. После синтаксического анализа документа функции Cucumber будет искать определения шагов, которые соответствуют предопределенным шагам Gherkin для выполнения.

«Чтобы было понятнее, давайте взглянем на следующий шаг:

Given I have registered a course in Baeldung

И определение шага:

@Given("I have registered a course in Baeldung")
public void verifyAccount() {
    // method implementation
}

Когда Cucumber читает данный шаг, он будет искать определения шагов, чьи аннотирующие шаблоны соответствуют тексту огурца.

4. Создание и запуск тестов

4.1. Написание файла функций

Давайте начнем с объявления сценариев и шагов в файле с именем, оканчивающимся на расширение .feature:

Feature: Testing a REST API
  Users should be able to submit GET and POST requests to a web service, 
  represented by WireMock

  Scenario: Data Upload to a web service
    When users upload data on a project
    Then the server should handle it and return a success status

  Scenario: Data retrieval from a web service
    When users want to get information on the 'Cucumber' project
    Then the requested data is returned

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

4.2. Настройка JUnit для работы с Cucumber

Чтобы JUnit знал о Cucumber и читал файлы функций во время работы, класс Cucumber должен быть объявлен как Runner. Нам также нужно указать JUnit место для поиска файлов функций и определений шагов.

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:Feature")
public class CucumberIntegrationTest {
    
}

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

4.3. Написание определений шагов

Когда Cucumber анализирует шаги, он будет искать методы, аннотированные ключевыми словами Gherkin, чтобы найти соответствующие определения шагов.

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

Следующий метод полностью соответствует шагу корнишона. Метод будет использоваться для отправки данных в веб-службу REST:

@When("users upload data on a project")
public void usersUploadDataOnAProject() throws IOException {
    
}

А вот метод, соответствующий шагу корнишона и принимающий аргумент из текста, который будет использоваться для получения информации из веб-службы REST:

@When("users want to get information on the {string} project")
public void usersGetInformationOnAProject(String projectName) throws IOException {
    
}

Как видите, метод usersGetInformationOnAProject принимает строковый аргумент, являющийся именем проекта. Этот аргумент объявлен как {string} в аннотации, и здесь он соответствует Cucumber в тексте шага.

В качестве альтернативы мы могли бы использовать регулярное выражение:

@When("^users want to get information on the '(.+)' project$")
public void usersGetInformationOnAProject(String projectName) throws IOException {
    
}

Обратите внимание, что «^» и «$» обозначают начало и конец регулярного выражения соответственно. Принимая во внимание, что «(.+)» соответствует параметру String.

Мы предоставим рабочий код для обоих вышеперечисленных методов в следующем разделе.

4.4. Создание и запуск тестов

Сначала мы начнем со структуры JSON, чтобы проиллюстрировать данные, загружаемые на сервер с помощью запроса POST и загружаемые на клиент с помощью GET. Эта структура сохраняется в поле jsonString и показана ниже:

{
    "testing-framework": "cucumber",
    "supported-language": 
    [
        "Ruby",
        "Java",
        "Javascript",
        "PHP",
        "Python",
        "C++"
    ],

    "website": "cucumber.io"
}

Для демонстрации REST API мы используем сервер WireMock:

WireMockServer wireMockServer = new WireMockServer(options().dynamicPort());

Кроме того, мы будем использовать Apache HttpClient API для представления клиент, используемый для подключения к серверу:

CloseableHttpClient httpClient = HttpClients.createDefault();

Теперь давайте перейдем к написанию тестового кода в определениях шагов. Сначала мы сделаем это для метода usersUploadDataOnAProject.

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

wireMockServer.start();

Использование WireMock API для заглушки службы REST:

configureFor("localhost", wireMockServer.port());
stubFor(post(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json"))
  .withRequestBody(containing("testing-framework"))
  .willReturn(aResponse().withStatus(200)));

Теперь отправьте запрос POST с содержимым, взятым из jsonString поле, объявленное выше, серверу:

HttpPost request = new HttpPost("http://localhost:" + wireMockServer.port() + "/create");
StringEntity entity = new StringEntity(jsonString);
request.addHeader("content-type", "application/json");
request.setEntity(entity);
HttpResponse response = httpClient.execute(request);

Следующий код подтверждает, что запрос POST был успешно получен и обработан:

assertEquals(200, response.getStatusLine().getStatusCode());
verify(postRequestedFor(urlEqualTo("/create"))
  .withHeader("content-type", equalTo("application/json")));

После использования сервер должен остановиться:

wireMockServer.stop();

Второй Метод, который мы реализуем здесь, — это usersGetInformationOnAProject(String projectName). Как и в первом тесте, нам нужно запустить сервер, а затем заглушить службу REST:

wireMockServer.start();

configureFor("localhost", wireMockServer.port());
stubFor(get(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json"))
  .willReturn(aResponse().withBody(jsonString)));

Отправка запроса GET и получение ответа:

HttpGet request = new HttpGet("http://localhost:" + wireMockServer.port() + "/projects/" + projectName.toLowerCase());
request.addHeader("accept", "application/json");
HttpResponse httpResponse = httpClient.execute(request);

Мы преобразуем переменную httpResponse в строку, используя вспомогательный метод:

String responseString = convertResponseToString(httpResponse);

Вот реализация этого вспомогательного метода преобразования:

private String convertResponseToString(HttpResponse response) throws IOException {
    InputStream responseStream = response.getEntity().getContent();
    Scanner scanner = new Scanner(responseStream, "UTF-8");
    String responseString = scanner.useDelimiter("\\Z").next();
    scanner.close();
    return responseString;
}

Следующее проверяет весь процесс:

assertThat(responseString, containsString("\"testing-framework\": \"cucumber\""));
assertThat(responseString, containsString("\"website\": \"cucumber.io\""));
verify(getRequestedFor(urlEqualTo("/projects/cucumber"))
  .withHeader("accept", equalTo("application/json")));

Наконец, остановите сервер, как описано выше.

5. Параллельное выполнение функций

Cucumber-JVM изначально поддерживает параллельное выполнение тестов в нескольких потоках. Мы будем использовать JUnit вместе с плагином Maven Failsafe для выполнения бегунов. В качестве альтернативы мы могли бы использовать Maven Surefire.

«JUnit запускает файлы функций параллельно, а не сценарии, что означает, что все сценарии в файле функций будут выполняться одним и тем же потоком.

Давайте теперь добавим конфигурацию плагина:

<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${maven-failsafe-plugin.version}</version>
    <configuration>
        <includes>
            <include>CucumberIntegrationTest.java</include>
        </includes>
        <parallel>methods</parallel>
        <threadCount>2</threadCount>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Обратите внимание, что:

    parallel: могут быть классы, методы или и то, и другое — в нашем случае классы заставят каждый тестовый класс выполняться в отдельном потоке. threadCount: указывает, сколько потоков должно быть выделено для этого выполнения

Это все, что нам нужно сделать, чтобы запустить функции Cucumber параллельно.

6. Заключение

В этом руководстве мы рассмотрели основы Cucumber и то, как этот фреймворк использует предметно-ориентированный язык Gherkin для тестирования REST API.

Как обычно, все примеры кода, показанные в этом руководстве, доступны на GitHub.