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

Тестирование API

Введение​

Playwright может быть использован для доступа к REST API вашего приложения.

Иногда может возникнуть необходимость отправить запросы на сервер напрямую из Java, не загружая страницу и не выполняя в ней JavaScript-код. Несколько примеров, когда это может быть полезно:

  • Тестирование API вашего сервера.
  • Подготовка состояния на стороне сервера перед посещением веб-приложения в тесте.
  • Проверка постусловий на стороне сервера после выполнения некоторых действий в браузере.

Все это можно достичь с помощью методов APIRequestContext.

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

APIRequestContext может отправлять все виды HTTP(S) запросов по сети.

Следующий пример демонстрирует, как использовать Playwright для тестирования создания задач через GitHub API. Набор тестов будет выполнять следующие действия:

  • Создавать новый репозиторий перед запуском тестов.
  • Создавать несколько задач и проверять состояние сервера.
  • Удалять репозиторий после выполнения тестов.

Настройка​

GitHub API требует авторизации, поэтому мы настроим токен один раз для всех тестов. Заодно мы также установим baseURL, чтобы упростить тесты.

package org.example;

import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.Playwright;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;

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

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestGitHubAPI {
private static final String API_TOKEN = System.getenv("GITHUB_API_TOKEN");

private Playwright playwright;
private APIRequestContext request;

void createPlaywright() {
playwright = Playwright.create();
}

void createAPIRequestContext() {
Map<String, String> headers = new HashMap<>();
// Устанавливаем этот заголовок согласно рекомендациям GitHub.
headers.put("Accept", "application/vnd.github.v3+json");
// Добавляем токен авторизации ко всем запросам.
// Предполагается, что личный токен доступа доступен в окружении.
headers.put("Authorization", "token " + API_TOKEN);

request = playwright.request().newContext(new APIRequest.NewContextOptions()
// Все запросы, которые мы отправляем, идут на этот API-эндпоинт.
.setBaseURL("https://api.github.com")
.setExtraHTTPHeaders(headers));
}

@BeforeAll
void beforeAll() {
createPlaywright();
createAPIRequestContext();
}

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

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

@AfterAll
void afterAll() {
disposeAPIRequestContext();
closePlaywright();
}
}

Написание тестов​

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

package org.example;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.RequestOptions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

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

import static org.junit.jupiter.api.Assertions.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestGitHubAPI {
private static final String REPO = "test-repo-2";
private static final String USER = System.getenv("GITHUB_USER");
private static final String API_TOKEN = System.getenv("GITHUB_API_TOKEN");

private Playwright playwright;
private APIRequestContext request;

// ...

@Test
void shouldCreateBugReport() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Bug] report 1");
data.put("body", "Bug description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());

APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
assertTrue(issues.ok());
JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
JsonObject issue = null;
for (JsonElement item : json) {
JsonObject itemObj = item.getAsJsonObject();
if (!itemObj.has("title")) {
continue;
}
if ("[Bug] report 1".equals(itemObj.get("title").getAsString())) {
issue = itemObj;
break;
}
}
assertNotNull(issue);
assertEquals("Bug description", issue.get("body").getAsString(), issue.toString());
}

@Test
void shouldCreateFeatureRequest() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Feature] request 1");
data.put("body", "Feature description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());

APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
assertTrue(issues.ok());
JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
JsonObject issue = null;
for (JsonElement item : json) {
JsonObject itemObj = item.getAsJsonObject();
if (!itemObj.has("title")) {
continue;
}
if ("[Feature] request 1".equals(itemObj.get("title").getAsString())) {
issue = itemObj;
break;
}
}
assertNotNull(issue);
assertEquals("Feature description", issue.get("body").getAsString(), issue.toString());
}
}

Настройка и завершение​

Эти тесты предполагают, что репозиторий существует. Вероятно, вы захотите создать новый перед запуском тестов и удалить его после. Используйте хуки @BeforeAll и @AfterAll для этого.

public class TestGitHubAPI {
// ...

void createTestRepository() {
APIResponse newRepo = request.post("/user/repos",
RequestOptions.create().setData(Collections.singletonMap("name", REPO)));
assertTrue(newRepo.ok(), newRepo.text());
}

@BeforeAll
void beforeAll() {
createPlaywright();
createAPIRequestContext();
createTestRepository();
}

void deleteTestRepository() {
if (request != null) {
APIResponse deletedRepo = request.delete("/repos/" + USER + "/" + REPO);
assertTrue(deletedRepo.ok());
}
}
// ...

@AfterAll
void afterAll() {
deleteTestRepository();
disposeAPIRequestContext();
closePlaywright();
}
}

Полный пример теста​

Вот полный пример теста API:

package org.example;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.RequestOptions;
import org.junit.jupiter.api.*;

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

import static org.junit.jupiter.api.Assertions.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestGitHubAPI {
private static final String REPO = "test-repo-2";
private static final String USER = System.getenv("GITHUB_USER");
private static final String API_TOKEN = System.getenv("GITHUB_API_TOKEN");

private Playwright playwright;
private APIRequestContext request;

void createPlaywright() {
playwright = Playwright.create();
}

void createAPIRequestContext() {
Map<String, String> headers = new HashMap<>();
// Устанавливаем этот заголовок согласно рекомендациям GitHub.
headers.put("Accept", "application/vnd.github.v3+json");
// Добавляем токен авторизации ко всем запросам.
// Предполагается, что личный токен доступа доступен в окружении.
headers.put("Authorization", "token " + API_TOKEN);

request = playwright.request().newContext(new APIRequest.NewContextOptions()
// Все запросы, которые мы отправляем, идут на этот API-эндпоинт.
.setBaseURL("https://api.github.com")
.setExtraHTTPHeaders(headers));
}

void createTestRepository() {
APIResponse newRepo = request.post("/user/repos",
RequestOptions.create().setData(Collections.singletonMap("name", REPO)));
assertTrue(newRepo.ok(), newRepo.text());
}

@BeforeAll
void beforeAll() {
createPlaywright();
createAPIRequestContext();
createTestRepository();
}

void deleteTestRepository() {
if (request != null) {
APIResponse deletedRepo = request.delete("/repos/" + USER + "/" + REPO);
assertTrue(deletedRepo.ok());
}
}

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

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

@AfterAll
void afterAll() {
deleteTestRepository();
disposeAPIRequestContext();
closePlaywright();
}

@Test
void shouldCreateBugReport() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Bug] report 1");
data.put("body", "Bug description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());

APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
assertTrue(issues.ok());
JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
JsonObject issue = null;
for (JsonElement item : json) {
JsonObject itemObj = item.getAsJsonObject();
if (!itemObj.has("title")) {
continue;
}
if ("[Bug] report 1".equals(itemObj.get("title").getAsString())) {
issue = itemObj;
break;
}
}
assertNotNull(issue);
assertEquals("Bug description", issue.get("body").getAsString(), issue.toString());
}

@Test
void shouldCreateFeatureRequest() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Feature] request 1");
data.put("body", "Feature description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());

APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
assertTrue(issues.ok());
JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
JsonObject issue = null;
for (JsonElement item : json) {
JsonObject itemObj = item.getAsJsonObject();
if (!itemObj.has("title")) {
continue;
}
if ("[Feature] request 1".equals(itemObj.get("title").getAsString())) {
issue = itemObj;
break;
}
}
assertNotNull(issue);
assertEquals("Feature description", issue.get("body").getAsString(), issue.toString());
}
}

Смотрите экспериментальную интеграцию с JUnit для автоматической инициализации объектов Playwright и других возможностей.

Подготовка состояния сервера через API вызовы​

Следующий тест создает новую задачу через API, а затем переходит к списку всех задач в проекте, чтобы проверить, что она появляется в начале списка. Проверка выполняется с использованием LocatorAssertions.

public class TestGitHubAPI {
@Test
void lastCreatedIssueShouldBeFirstInTheList() {
Map<String, String> data = new HashMap<>();
data.put("title", "[Feature] request 1");
data.put("body", "Feature description");
APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
RequestOptions.create().setData(data));
assertTrue(newIssue.ok());

page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first();
assertThat(firstIssue).hasText("[Feature] request 1");
}
}

Проверка состояния сервера после выполнения действий пользователя​

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

public class TestGitHubAPI {
@Test
void lastCreatedIssueShouldBeOnTheServer() {
page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
page.locator("text=New Issue").click();
page.locator("[aria-label='Title']").fill("Bug report 1");
page.locator("[aria-label='Comment body']").fill("Bug description");
page.locator("text=Submit new issue").click();
String issueId = page.url().substring(page.url().lastIndexOf('/'));

APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
assertThat(newIssue).isOK();
assertTrue(newIssue.text().contains("Bug report 1"));
}
}

Повторное использование состояния аутентификации​

Веб-приложения используют аутентификацию на основе куки или токенов, где аутентифицированное состояние хранится в виде cookies. Playwright предоставляет метод APIRequestContext.storageState(), который может быть использован для получения состояния хранилища из аутентифицированного контекста, а затем создания новых контекстов с этим состоянием.

Состояние хранилища взаимозаменяемо между BrowserContext и APIRequestContext. Вы можете использовать его для входа через API вызовы, а затем создать новый контекст с уже установленными куки. Следующий код извлекает состояние из аутентифицированного APIRequestContext и создает новый BrowserContext с этим состоянием.

APIRequestContext requestContext = playwright.request().newContext(
new APIRequest.NewContextOptions().setHttpCredentials("user", "passwd"));
requestContext.get("https://api.example.com/login");
// Сохраните состояние хранилища в переменную.
String state = requestContext.storageState();

// Создайте новый контекст с сохраненным состоянием хранилища.
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setStorageState(state));