跳到主要内容

API 测试

简介

Playwright 可用于访问应用程序的 REST API。

有时,你可能希望直接从 .NET 向服务器发送请求,而无需加载页面并在其中运行 JavaScript 代码。以下是一些可能会用到这种方式的场景示例:

  • 测试服务器 API。
  • 在测试中访问 Web 应用程序之前,准备服务器端状态。
  • 在浏览器中执行某些操作后,验证服务器端的后置条件。

所有这些都可以通过 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 检查该问题是否已创建:

// 如果要使用 Page 类,请确保从 PageTest 继承。
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");
}
}

复用身份验证状态

Web 应用程序使用基于 cookie 或基于令牌的身份验证,其中经过身份验证的状态存储为 cookies。Playwright 提供了 ApiRequestContext.StorageStateAsync() 方法,可用于从经过身份验证的上下文检索存储状态,然后使用该状态创建新的上下文。

存储状态在 BrowserContextAPIRequestContext 之间是可互换的。你可以使用它通过 API 调用登录,然后创建一个已经包含 cookie 的新上下文。以下代码片段从经过身份验证的 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 });