Перейти к основному содержимому

Тестирование REST API с помощью Cucumber

· 9 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

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

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

2. Огурец – язык огурца

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

2.1. Знакомство с огурцами

Огурец — это построчный язык, использующий окончания строк, отступы и ключевые слова для определения документов. Каждая непустая строка обычно начинается с ключевого слова 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

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

2.2. Особенность

Мы используем файл 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 ForEach

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

@Given("I have registered a course in ForEach")
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 при условии, что этот каталог будет загружен в путь к классам во время выполнения, например, src/main/resources .

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

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

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

}

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

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

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

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

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

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

}

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

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

}

Как видите, метод usersGetInformationOnAProject принимает аргумент String , который является именем проекта. Этот аргумент объявлен как {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());

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

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 .