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 需要身份驗證,因此我們將為所有測試設定一次令牌。同時,我們也會設定 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() 方法,可用於從已認證的上下文中擷取儲存狀態,然後使用該狀態建立新的上下文。

儲存狀態在 [BrowserContext] 和 [APIRequestContext] 之間是可互換的。您可以使用它透過 API 呼叫登入,然後建立一個已包含 cookies 的新上下文。以下程式碼片段從已認證的 [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 });