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

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

Введение

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

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

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

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

Следующие примеры опираются на пакет Microsoft.Playwright.MSTest, который создает экземпляры Playwright и Page для каждого теста.

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

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

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

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

Настройка

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

using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;

namespace PlaywrightTests;

[TestClass]
public class TestGitHubAPI : PlaywrightTest
{
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");

private IAPIRequestContext Request = null!;

[TestInitialize]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
}

private async Task CreateAPIRequestContext()
{
var headers = new Dictionary<string, string>();
// Устанавливаем этот заголовок согласно рекомендациям GitHub.
headers.Add("Accept", "application/vnd.github.v3+json");
// Добавляем токен авторизации ко всем запросам.
// Предполагается, что личный токен доступа доступен в окружении.
headers.Add("Authorization", "token " + API_TOKEN);

Request = await this.Playwright.APIRequest.NewContextAsync(new() {
// Все запросы, которые мы отправляем, идут на этот API-эндпоинт.
BaseURL = "https://api.github.com",
ExtraHTTPHeaders = headers,
});
}

[TestCleanup]
public async Task TearDownAPITesting()
{
await Request.DisposeAsync();
}
}

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

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

using System.Text.Json;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;

namespace PlaywrightTests;

[TestClass]
public class TestGitHubAPI : PlaywrightTest
{
static string REPO = "test";
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");

private IAPIRequestContext Request = null!;

[TestMethod]
public async Task ShouldCreateBugReport()
{
var data = new Dictionary<string, string>
{
{ "title", "[Bug] report 1" },
{ "body", "Bug description" }
};
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();

var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
var issuesJsonResponse = await issues.JsonAsync();
JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
{
if (issueObj.TryGetProperty("title", out var title) == true)
{
if (title.GetString() == "[Bug] report 1")
{
issue = issueObj;
}
}
}
Assert.IsNotNull(issue);
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
}

[TestMethod]
public async Task ShouldCreateFeatureRequests()
{
var data = new Dictionary<string, string>
{
{ "title", "[Feature] request 1" },
{ "body", "Feature description" }
};
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();

var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
var issuesJsonResponse = await issues.JsonAsync();

JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
{
if (issueObj.TryGetProperty("title", out var title) == true)
{
if (title.GetString() == "[Feature] request 1")
{
issue = issueObj;
}
}
}
Assert.IsNotNull(issue);
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
}

// ...
}

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

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

using System.Text.Json;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;

namespace PlaywrightTests;

[TestClass]
public class TestGitHubAPI : PlaywrightTest
{
// ...
[TestInitialize]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
await CreateTestRepository();
}

private async Task CreateTestRepository()
{
var resp = await Request.PostAsync("/user/repos", new()
{
DataObject = new Dictionary<string, string>()
{
["name"] = REPO,
},
});
await Expect(resp).ToBeOKAsync();
}

[TestCleanup]
public async Task TearDownAPITesting()
{
await DeleteTestRepository();
await Request.DisposeAsync();
}

private async Task DeleteTestRepository()
{
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
await Expect(resp).ToBeOKAsync();
}
}

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

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

using System.Text.Json;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;

namespace PlaywrightTests;

[TestClass]
public class TestGitHubAPI : PlaywrightTest
{
static string REPO = "test-repo-2";
static string USER = Environment.GetEnvironmentVariable("GITHUB_USER");
static string? API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN");

private IAPIRequestContext Request = null!;

[TestMethod]
public async Task ShouldCreateBugReport()
{
var data = new Dictionary<string, string>
{
{ "title", "[Bug] report 1" },
{ "body", "Bug description" }
};
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();

var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
var issuesJsonResponse = await issues.JsonAsync();
JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
{
if (issueObj.TryGetProperty("title", out var title) == true)
{
if (title.GetString() == "[Bug] report 1")
{
issue = issueObj;
}
}
}
Assert.IsNotNull(issue);
Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString());
}

[TestMethod]
public async Task ShouldCreateFeatureRequests()
{
var data = new Dictionary<string, string>
{
{ "title", "[Feature] request 1" },
{ "body", "Feature description" }
};
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();

var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues");
await Expect(newIssue).ToBeOKAsync();
var issuesJsonResponse = await issues.JsonAsync();

JsonElement? issue = null;
foreach (JsonElement issueObj in issuesJsonResponse?.EnumerateArray())
{
if (issueObj.TryGetProperty("title", out var title) == true)
{
if (title.GetString() == "[Feature] request 1")
{
issue = issueObj;
}
}
}
Assert.IsNotNull(issue);
Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString());
}

[TestInitialize]
public async Task SetUpAPITesting()
{
await CreateAPIRequestContext();
await CreateTestRepository();
}

private async Task CreateAPIRequestContext()
{
var headers = new Dictionary<string, string>
{
// Устанавливаем этот заголовок согласно рекомендациям GitHub.
{ "Accept", "application/vnd.github.v3+json" },
// Добавляем токен авторизации ко всем запросам.
// Предполагается, что личный токен доступа доступен в окружении.
{ "Authorization", "token " + API_TOKEN }
};

Request = await Playwright.APIRequest.NewContextAsync(new()
{
// Все запросы, которые мы отправляем, идут на этот API-эндпоинт.
BaseURL = "https://api.github.com",
ExtraHTTPHeaders = headers,
});
}

private async Task CreateTestRepository()
{
var resp = await Request.PostAsync("/user/repos", new()
{
DataObject = new Dictionary<string, string>()
{
["name"] = REPO,
},
});
await Expect(resp).ToBeOKAsync();
}

[TestCleanup]
public async Task TearDownAPITesting()
{
await DeleteTestRepository();
await Request.DisposeAsync();
}

private async Task DeleteTestRepository()
{
var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO);
await Expect(resp).ToBeOKAsync();
}
}

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

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

class TestGitHubAPI : PageTest
{
[TestMethod]
public async Task LastCreatedIssueShouldBeFirstInTheList()
{
var data = new Dictionary<string, string>
{
{ "title", "[Feature] request 1" },
{ "body", "Feature description" }
};
var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data });
await Expect(newIssue).ToBeOKAsync();

// При наследовании от 'PlaywrightTest' вы получаете только экземпляр Playwright. Чтобы получить экземпляр Page, либо запустите
// браузер, контекст и страницу вручную, либо наследуйтесь от 'PageTest', который запустит их за вас.
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
var firstIssue = Page.Locator("a[data-hovercard-type='issue']").First;
await Expect(firstIssue).ToHaveTextAsync("[Feature] request 1");
}
}

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

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

// Убедитесь, что вы наследуетесь от PageTest, если хотите использовать класс Page.
class GitHubTests : PageTest
{
[TestMethod]
public async Task LastCreatedIssueShouldBeOnTheServer()
{
await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues");
await Page.Locator("text=New Issue").ClickAsync();
await Page.Locator("[aria-label='Title']").FillAsync("Bug report 1");
await Page.Locator("[aria-label='Comment body']").FillAsync("Bug description");
await Page.Locator("text=Submit new issue").ClickAsync();
var issueId = Page.Url.Substring(Page.Url.LastIndexOf('/'));

var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
await Expect(newIssue).ToBeOKAsync();
StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1");
}
}

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

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

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

var requestContext = await Playwright.APIRequest.NewContextAsync(new()
{
HttpCredentials = new()
{
Username = "user",
Password = "passwd"
},
});
await requestContext.GetAsync("https://api.example.com/login");
// Сохраните состояние хранилища в переменную.
var state = await requestContext.StorageStateAsync();

// Создайте новый контекст с сохраненным состоянием хранилища.
var context = await Browser.NewContextAsync(new() { StorageState = state });