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

Тестирование 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 });