APIテスト
はじめに
Playwrightは、アプリケーションのREST APIにアクセスするために使用できます。
ページをロードしてJavaScriptコードを実行することなく、.NETから直接サーバーにリクエストを送信したい場合があります。いくつかの便利な例を挙げます。
- サーバーAPIをテストする。
- テストでウェブアプリケーションにアクセスする前に、サーバー側の状態を準備する。
- ブラウザでいくつかのアクションを実行した後、サーバー側の事後条件を検証する。
これらはすべて、APIRequestContextメソッドを使用して実現できます。
以下の例は、各テストでPlaywrightおよびPageインスタンスを作成するMicrosoft.Playwright.MSTest
パッケージに依存しています。
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>();
// 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");
}
}
認証状態の再利用
ウェブアプリは、認証状態がクッキーとして保存されるクッキーベースまたはトークンベースの認証を使用します。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");
// 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 });