Тестирование API в Playwright/Java. POST-запросы

Подготовка

Прежде чем приступить к написанию тестовых сценариев опишем, что нужно поставить и настроить.

Ставим и настраиваем

Ниже перечислены необходимые условия, которые должны быть установлены на машине. 

  1. Java JDK 17 
  2. IntelliJ IDE или любая другая IDE
  3. Maven

Создание нового проекта 

Будем создавать Maven-проект с помощью IntelliJ IDE. Шаги создания Maven-проекта:

  1. Откройте IntelliJ, перейдите в меню Файл >> Новый >> Проект: 
  1. В окне Новый проект введите:
  • Название проекта 
  • Путь, где будет сохранен проект 
  • Выберите версию JDK — я использую JDK 17 
  • Archetype — введите слово «quickstart» и выберите maven-archetype-quickstart из результатов 
  • Нажмите на кнопку Создать, чтобы создать проект

После этого проект Maven должен быть создан в IntelliJ IDE. 

Если вы новичок в IntelliJ, вот ютуб по созданию Maven-проекта (на английском).

Обновляем зависимости

После успешного создания проекта нужно добавить в pom.xml следующие зависимости и плагины: 

  1. Playwright — Java 
  2. TestNG 
  3. Lombok — для использования паттерна builder и генерации POJO во время выполнения 
  4. Gson — для сериализации 
  5. Datafaker — для генерации тестовых данных во время выполнения 
  6. Плагин Maven Surefire
  7. Плагин Maven Compiler.

Нужные файлы на Гитхабе

Файл: pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>io.github.mfaisalkhatri</groupId>
    <artifactId>api-testing-playwright</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>api-testing-playwright</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <playwright-version>1.47.0</playwright-version>
        <testng-version>7.10.2</testng-version>
        <lombok-version>1.18.34</lombok-version>
        <datafaker-version>2.3.1</datafaker-version>
        <gson-version>2.11.0</gson-version>
        <maven-compiler-plugin-version>3.13.0</maven-compiler-plugin-version>
        <maven-surefire-plugin-version>3.3.1</maven-surefire-plugin-version>
        <java-release-version>17</java-release-version>
        <suite-xml>test-suite/testng.xml</suite-xml>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.microsoft.playwright</groupId>
            <artifactId>playwright</artifactId>
            <version>${playwright-version}</version>
        </dependency>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng-version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok-version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>net.datafaker</groupId>
            <artifactId>datafaker</artifactId>
            <version>${datafaker-version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>${gson-version}</version>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>${maven-compiler-plugin-version}</version>
                    <configuration>
                        <release>${java-release-version}</release>
                        <encoding>UTF-8</encoding>
                        <forceJavacCompilerUse>true</forceJavacCompilerUse>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>${maven-surefire-plugin-version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>test</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <suiteXmlFiles>
                            <suiteXmlFile>${suite-xml}</suiteXmlFile>
                        </suiteXmlFiles>
                        <argLine>-Dfile.encoding=UTF-8 -Xdebug -Xnoagent</argLine>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-enforcer-plugin</artifactId>
                    <version>${maven-enforcer-version}</version>
                    <executions>
                        <execution>
                            <id>enforce-maven</id>
                            <goals>
                                <goal>enforce</goal>
                            </goals>
                            <configuration>
                                <rules>
                                    <requireMavenVersion>
                                        <version>3.0</version>
                                    </requireMavenVersion>
                                </rules>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

Обновление версий зависимостей в блоке Properties является хорошей практикой и помогает проверять и обновлять версии maven в проекте. 

После создания проекта Maven и обновления зависимостей в pom.xml проект будет готов. 

Тестируемое приложение 

Мы будем использовать бесплатные API restful-ecommerce (ссылка). Он предоставляет список API управления заказами, связанных с электронной коммерцией, для создания, получения, обновления и удаления заказов. Кроме того, API предоставляет JWT-токен, который можно использовать в качестве инструмента безопасности при обновлении и удалении записей заказов.

Этот проект может быть установлен на локальной машине с помощью NodeJS или с помощью Docker. Более подробно об этом здесь.

Что такое POST-запрос

Прежде чем мы погрузимся в мир автоматизации тестирования API, давайте немного освежим знания о HTTP-методе POST.

Запрос POST используется для отправки данных на сервер для создания новой записи в системе. В идеале POST-запрос предполагает предоставление некоторых данных вместе с запросом. Тело этих данных зависит от заголовка Content-Type. Чаще всего для REST API для передачи данных в POST-запросе используется JSON. 

Однако в зависимости от Content-Type для передачи данных в POST-запросе также могут использоваться XML и другие типы данных. POST-запросы обычно возвращают в ответ код состояния 201. 

В следующем примере API restful-ecommerce, POST-запрос /addOrder отправляет данные на сервер для создания нового заказа (заказов).

После успешного выполнения POST-запроса в системе создаются необходимые данные. В ответ возвращается код состояния 201, вместе с подтверждением и информацией о созданном в системе заказе.

POST API в Playwright/Java

Мы должны быть знакомы с некоторыми интерфейсами и классами Playwright, которые могут применяться для автоматизации тестирования API. В данном случае мы будем использовать API restful-ecommerce для написания тестов API по счастливым и печальным сценариям. Будем последовательно выполнять код и комментировать детали, чтобы убедиться, что все понятно.

Тестовый сценарий 1 — Добавление заказов

  1. Запустите службу restful-ecommerce 
  2. Сгенерируйте тестовые данные во время выполнения, с помощью Lombok и DataFaker 
  3. Создайте 4 различных заказа в системе с помощью API-запроса POST /addOrder, предоставив необходимый набор данных в формате JSON 
  4. Убедитесь, что в ответ получен статус-код 201 
  5. Убедитесь, что данные заказа сгенерированы правильно, как указано в POST-запросе

Реализация теста 

Для реализации тестового сценария мы создадим новый пакет в папке src/test/ и назовем его «restfulecommerce«. В дальнейшем будем использовать этот пакет для создания тестовых классов и управления тестовыми данными. 

Первым шагом на пути к реализации будет написание класса BaseTest, который будет заниматься установкой и настройкой Playwright. Этот класс BaseTest будет реюзабельным (повторно использоваться) в тестовых классах, чтобы избежать дублирования кода и упростить сопровождение тестовых сценариев. 

Написание BaseTest 

Создадим новый java-класс с именем BaseTest внутри пакета «restfulecommerce«. Интерфейсы Playwright и APIRequestContext будут объявлены в этом классе глобально, поскольку нам нужно будет вызывать эти интерфейсы в методах setup() и tearDown() в одном и том же классе.

public class BaseTest {

    protected Playwright playwright;
    protected APIRequestContext request;
    private static final String BASE_URL = "http://localhost:3004";

    //...
}

Переменная BASE_URL будет объявлена как static final глобально, так как она будет оставаться постоянной на протяжении всего цикла выполнения тестов. URL = localhost, так как приложение restful-ecommerce будет запускаться локально, а тесты будут выполняться на соответствующих API. 

Два новых метода — createPlaywright() и createAPIRequestContext() — создаются с модификатором доступа private, так как эти методы будут использоваться только в этом классе BaseTest.

private void createPlaywright() {
    playwright = Playwright.create();
}
private void createAPIRequestContext() {
    Map<String, String> headers = new HashMap<>();
    headers.put("Content-Type", "application/json");

    request = playwright.request().newContext(new APIRequest.NewContextOptions()
            .setBaseURL(BASE_URL)
            .setExtraHTTPHeaders(headers));
}

Метод createPlaywright() создаст новый экземпляр Playwright. Метод createAPIRequestContext() задаст конфигурацию запроса API, например, установит общие заголовки запроса и базовый url. Он также создаст новый экземпляр APIRequestContext, который будет использоваться в тестах. 

Создается новый метод setup() с аннотацией @BeforeTest, который будет запущен до начала любого из тестов. Он вызовет методы createPlaywright() и createAPIRequestContext(), которые в конечном итоге подготовят сцену для запуска API-тестов.

@BeforeClass
public void setup() {
    createPlaywright();
    createAPIRequestContext();
}

Еще два созданных метода — closePlaywright(), который закрывает сессию Playwright, и disposeAPIRequestContext(), который отбрасывает все ответы, возвращенные APIRequestContext.get() и другими подобными методами, которые хранятся в памяти. 

Эти методы имеют модификаторы доступа private, поскольку они предназначены только для этого класса BaseTest.

private void closePlaywright() {
    if (playwright != null) {
        playwright.close();
        playwright = null;
    }
}

private void disposeAPIRequestContext() {
    if (request != null) {
        request.dispose();
        request = null;
    }
}

Эти два метода будут вызваны в методе tearDown(), который имеет аннотацию @AfterClass TestNG. Этот метод вызывает методы closePlaywright() и disposeAPIRequestContext() и будет выполнен после завершения теста, что позволит плавно закрыть сессию Playwright. 

Написание теста 

Создадим новый java-класс — HappyPathTests внутри существующего пакета «restfulecommerce«. Этот класс HappyPathTests будет расширять класс BaseTest, поскольку он позволит повторно использовать публичные методы и переменные, определенные в классе BaseTest.

public class HappyPathTests extends BaseTest{

    private List<OrderData> orderList;
  
    //..

}

    @BeforeClass
    public void testSetup() {
        this.orderList = new ArrayList<>();
    }

Метод testSetup() инстанцирует List<OrderData> для создания массива данных о заказах (тело POST-запроса должно быть предоставлено в формате JSON Array), он остается глобальным, так как мы будем использовать его в различных тестовых методах для проверки заказов.

Теперь мы создадим новый тестовый метод testShouldCreateNewOrders() для проверки POST-запроса на создание новых заказов.

@Test
public void testShouldCreateNewOrders() {

    final int totalOrders = 4;

    for (int i = 0; i < totalOrders; i++) {
        this.orderList.add(getNewOrder());
    }

    final APIResponse response = this.request.post("/addOrder", RequestOptions.create()
            .setData(this.orderList));

    final JSONObject responseObject = new JSONObject(response.text());
    final JSONArray ordersArray = responseObject.getJSONArray("orders");

    assertEquals(response.status(), 201);
    assertEquals(responseObject.get("message"), "Orders added successfully!");
    assertNotNull(ordersArray.getJSONObject(0).get("id"));
    assertEquals(this.orderList.get(0).getUserId(), ordersArray.getJSONObject(0).get("user_id"));
    assertEquals(this.orderList.get(0).getProductId(), ordersArray.getJSONObject(0).get("product_id"));
    assertEquals(this.orderList.get(0).getTotalAmt(), ordersArray.getJSONObject(0).get("total_amt"));
}

Мы добавим в список заказов в общей сложности 4 заказа. Эти заказы будут предоставлены в качестве тела метода запроса /addOrderPOST

Ниже приведено тело запроса, которое нужно предоставить в запросе для генерации заказов.

[{
    "user_id": "1",
    "product_id": "1",
    "product_name": "iPhone",
    "product_amount": 500.00,
    "qty": 1,
    "tax_amt": 5.99,
    "total_amt": 505.99
},
{
    "user_id": "1",
    "product_id": "2",
    "product_name": "iPad",
    "product_amount": 699.00,
    "qty": 1,
    "tax_amt": 7.99,
    "total_amt": 706.99
}]

Чтобы сгенерировать данные для заказа, нам нужно создать класс POJO с точными именами полей, которые требуются для объекта заказа. Класс OrderData поможет в создании POJO.

@Getter
@Builder
public class OrderData {

    @SerializedName("user_id")
    private String userId;
    @SerializedName("product_id")
    private String productId;
    @SerializedName("product_name")
    private String productName;
    @SerializedName("product_amount")
    private int productAmount;
    private int qty;
    @SerializedName("tax_amt")
    private int taxAmt;
    @SerializedName("total_amt")
    private int totalAmt;

}

Поскольку имена полей JSON содержат символ «_» между ними, используется аннотация @SerializedName из библиотеки Gson, которая присоединит имя переменной, созданной в POJO, к сериализованному имени. 

Обратите внимание, что мы не создаем никаких методов типа Getter и Setter в этом классе, поскольку мы используем зависимость Lombok. Он автоматически создаст необходимые Getter-методы, поскольку мы использовали аннотацию @Getter из Lombok. Аналогично используется аннотация @Builder, которая позволяет использовать класс в качестве шаблона, избавляя от необходимости добавлять параметры в конструкторы класса. 

Данные заказа будут генерироваться с помощью статического метода getNewOrder(). Этот метод getNewOrder() берется из класса OrderDataBuilder, который доступен в пакете restfulecommerce/testdata.

public class OrderDataBuilder {

    private static final Faker FAKER = new Faker();

    public static OrderData getNewOrder() {
        int userId = FAKER.number().numberBetween(2, 4);
        int productId = FAKER.number().numberBetween(331,333);
        int productAmount = FAKER.number().numberBetween(400, 903);
        int quantity = FAKER.number().numberBetween(1, 5);
        int taxAmount = FAKER.number().numberBetween(10,50);
        int totalAmount = (productAmount*quantity)+taxAmount;


        return OrderData.builder()
                .userId(String.valueOf(userId))
                .productId(String.valueOf(productId))
                .productName(FAKER.commerce().productName())
                .productAmount(productAmount)
                .qty(quantity)
                .taxAmt(taxAmount)
                .totalAmt(totalAmount)
                .build();
    }

}

Тестовые данные, необходимые для заказов, будут генерироваться во время выполнения теста с помощью библиотеки Datafaker (вот она), зависимость для которой уже добавлена в файл pom.xml

Процесс довольно прост: создаем объект класса Faker, предоставляемый DataFaker, и используем его для вызова нужного метода данных и генерации данных во время выполнения теста. 

Мы будем добавлять 4 заказа в POST-запрос, следовательно, нам нужно вызвать метод getNewOrder() 4 раза в тесте. Для этого в тестовом методе будем использовать цикл For.

Далее для выполнения POST-запроса будут использованы следующие строки кода:

response — это переменная, определенная для типа APIResponse, которая хранит результат запроса API.

this.request.post(«/addOrder», …) отправит POST-запрос на эндпойнт «/addOrder». Этот эндпойнт будет обрабатывать добавление заказа. 

RequestOptions.create() создаст объект, который будет содержать различные параметры запроса, такие как заголовки запроса, тело и т. д. 

.setData(this.orderList) содержит список заказов, сохраненных в переменной orderList, всего 4 заказа, добавленных в цикле, который мы предоставили. Эти данные отправляются в качестве тела POST-вызова API-запроса. 

Следующая часть теста заключается в выполнении утверждений (assertions, здесь подробно) для проверки того, что отправленный POST-запрос успешно создал заказы в системе. Также необходимо проверить целостность данных о заказах, которые были созданы с помощью библиотеки Datafaker.

Первый assertion — проверка того, что в ответе получен правильный статус-код, то есть 201. 

В следующем утверждении мы проверяем, что поле «message» в объекте ответа содержит текст «Orders added successfully!!!«. Важным утверждением является проверка того, что идентификатор заказа успешно сгенерирован для каждого заказа. Поскольку мы не уверены в идентификаторе order id, т. е. сгенерированном id-номере заказа, мы будем проверять, что id не является нулевым значением. 

Нам нужно распарсить (разобрать) ответ и получить массивы JSON и объект JSON, чтобы получить поля для утверждения с соответствующими значениями.

Класс JSONObject из библиотеки org.json используется для разбора текста ответа, а класс JSONArray из той же библиотеки — для разбора массивов JSON из ответа. Эти объекты используются для получения необходимых данных из ответа для выполнения утверждений. 

Наконец, выполняются проверки целостности данных, чтобы детали заказа, такие как user_id, product_id, total_amt и т. д., были заполнены правильно и отображались так, как они были отправлены в запросе.

Выполнение теста 

Перед выполнением теста нам нужно убедиться (сверяя шаги здесь), что приложение restful-ecommerce запущено и работает. 

На следующем скриншоте IntelliJ IDE показано, что тест выполнен успешно и заказы были созданы в соответствии с POST-запросом.

Сценарий 2 — Некорректная нагрузка в запросе 

  1. Запустить службу restful-ecommerce 
  2. Выполнить POST-запрос с некорректной нагрузкой в запросе, когда поле product_id отсутствует в теле заказа 
  3. Убедиться, что в ответ получен статус-код 400 
  4. Убедиться, что в ответ получено сообщение «Each order must have user_id, product_id, product_name, product_amount, qty, tax_amt, and total_amt!«, и заказ не создан в системе.

Реализация теста

Создадим новый java-класс SadPathTests в существующем пакете «restfulecommerce«. Этот класс будет реализовывать все тестовые сценарии, связанные с печальными путями API restful-ecommerce. 

Под «печальными путями» вообще подразумеваются тестовые сценарии, в которых должно быть выдано внятное сообщение об ошибке, что улучшает пользовательский опыт. 

Например, тестовый сценарий, который мы только что рассмотрели, будет отправлять статус-код 400 в ответ, если в запросе отправлена неправильная нагрузка. Это позволит конечному пользователю понять, что он допустил ошибку при отправке POST-запроса, и соответствующим образом исправить свою ошибку.

В тестовом классе SadPathTests создан новый метод testShouldNotCreateOrder_WhenProductIdFieldIsMissing().

    @Test
    public void testShouldNotCreateOrder_WhenProductIdFieldIsMissing() {

        List<OrderData> orderList = new ArrayList<>();
        orderList.add(getOrderDataWithMissingProductId());

        final APIResponse response = this.request.post("/addOrder",
                RequestOptions.create().setData(orderList));

        final JSONObject responseObject = new JSONObject(response.text());
        assertEquals(response.status(), 400);
        assertEquals(responseObject.get("message"), "Each order must have user_id, product_id, product_name, product_amount, qty, tax_amt, and total_amt!");
    }

Реализация тестового сценария довольно проста: он создает новый список заказов ArrayList, чтобы обеспечить передачу тела запроса в формате Array

В классе OrderDataBuilder создается новый метод «getOrderDataWithMissingProductId()» для обработки тела запроса, которое мы должны отправить в POST-запросе.

    public static OrderData getOrderDataWithMissingProductId() {
        int userId = FAKER.number().numberBetween(2, 4);
        int productAmount = FAKER.number().numberBetween(400, 903);
        int quantity = FAKER.number().numberBetween(1, 5);
        int taxAmount = FAKER.number().numberBetween(10,50);
        int totalAmount = (productAmount*quantity)+taxAmount;


        return OrderData.builder()
                .userId(String.valueOf(userId))
                .productName(FAKER.commerce().productName())
                .productAmount(productAmount)
                .qty(quantity)
                .taxAmt(taxAmount)
                .totalAmt(totalAmount)
                .build();


    }

Следует отметить, что мы не указали поле product_id в вышеуказанном методе. 

POST-запрос выполняется, добавляя заказ с отсутствующим product_id, и, наконец, выполняются утверждения для проверки того, что в ответе получен статус-код 400 вместе с сообщением «Each order must have user_id, product_id, product_name, product_amount, qty, tax_amt, and total_amt!«. 

Выполнение теста 

Скриншот выполнения теста с помощью IntelliJ IDE показывает, что тест выполнен успешно.

Резюме

POST-запросы создают новый ресурс в системе и в идеале возвращают в ответе статус-код 201. Важной частью POST-запроса является тело запроса, которое должно быть представлено в требуемом формате в соответствии с заголовком Content-Type. В теле запроса был использован формат JSON, так как он был необходим приложению restful-ecommerce.

MFKhatri


Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас