Skip to main content

API 測試

簡介

Playwright 可以用來存取你的應用程式的 REST API。

有時你可能想直接從 .NET 發送請求到伺服器,而不需要載入頁面並在其中執行 js 程式碼。以下是一些可能會派上用場的範例:

  • 測試你的伺服器 API。
  • 在測試中訪問 web 應用程式之前準備伺服器端狀態。
  • 在瀏覽器中執行一些操作後驗證伺服器端後置條件。

所有這些都可以通過 APIRequestContext 方法實現。

以下範例依賴 Microsoft.Playwright.MSTest 套件,它為每個測試建立一個 Playwright 和 Page 實例。

撰寫 API 測試

APIRequestContext 可以透過網路發送各種 HTTP(S) 請求。

以下範例展示如何使用 Playwright 測試通過 GitHub API 建立問題。測試套件將執行以下操作:

  • 建立一個新的儲存庫後執行測試。
  • 建立一些問題並驗證伺服器狀態。
  • 執行測試後刪除儲存庫。

設定

GitHub API 需要授權,所以我們將為所有測試設定一次 token。在此同時,我們也會設定 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>();
// We set this header per GitHub guidelines.
headers.Add("Accept", "application/vnd.github.v3+json");
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
headers.Add("Authorization", "token " + API_TOKEN);

Request = await this.Playwright.APIRequest.NewContextAsync(new() {
// All requests we send go to this API endpoint.
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>
{
// We set this header per GitHub guidelines.
{ "Accept", "application/vnd.github.v3+json" },
// Add authorization token to all requests.
// Assuming personal access token available in the environment.
{ "Authorization", "token " + API_TOKEN }
};

Request = await Playwright.APIRequest.NewContextAsync(new()
{
// All requests we send go to this API endpoint.
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();

// When inheriting from 'PlaywrightTest' it only gives you a Playwright instance. To get a Page instance, either start
// a browser, context, and page manually or inherit from 'PageTest' which will launch it for you.
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 檢查是否已建立:

// Make sure to extend from PageTest if you want to use the Page class.
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 apps 使用基於 cookie 或基於 token 的身份驗證,已驗證的狀態會儲存為 cookies。Playwright 提供 ApiRequestContext.StorageStateAsync() 方法,可用於從已驗證的 context 中檢索儲存狀態,然後使用該狀態建立新的 context。

儲存狀態在 BrowserContextAPIRequestContext 之間是可互換的。你可以使用它通過 API 呼叫登入,然後建立一個已經包含 cookies 的新 context。以下程式碼片段從已驗證的 APIRequestContext 中檢索狀態,並使用該狀態建立一個新的 BrowserContext

var requestContext = await Playwright.APIRequest.NewContextAsync(new()
{
HttpCredentials = new()
{
Username = "user",
Password = "passwd"
},
});
await requestContext.GetAsync("https://api.example.com/login");
// Save storage state into a variable.
var state = await requestContext.StorageStateAsync();

// Create a new context with the saved storage state.
var context = await Browser.NewContextAsync(new() { StorageState = state });