diff --git a/docs/src/api-testing-csharp.md b/docs/src/api-testing-csharp.md new file mode 100644 index 0000000000..a316426568 --- /dev/null +++ b/docs/src/api-testing-csharp.md @@ -0,0 +1,410 @@ +--- +id: api-testing +title: "API testing" +--- + +Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of +your application. + +Sometimes you may want to send requests to the server directly from .NET without loading a page and running js code in it. +A few examples where it may come in handy: +- Test your server API. +- Prepare server side state before visiting the web application in a test. +- Validate server side post-conditions after running some actions in the browser. + +All of that could be achieved via [APIRequestContext] methods. + +The following examples rely on the [`Microsoft.Playwright.NUnit`](./test-runners.md) package which creates a Playwright and Page instance for each test. + + + +## Writing API Test + +[APIRequestContext] can send all kinds of HTTP(S) requests over network. + +The following example demonstrates how to use Playwright to test issues creation via [GitHub API](https://docs.github.com/en/rest). The test suite will do the following: +- Create a new repository before running tests. +- Create a few issues and validate server state. +- Delete the repository after running tests. + +### Configure + +GitHub API requires authorization, so we'll configure the token once for all tests. While at it, we'll also set the `baseURL` to simplify the tests. + +```csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; +using NUnit.Framework; + +namespace Playwright.TestingHarnessTest.NUnit +{ + + public class TestGitHubAPI : PlaywrightTest + { + static string API_TOKEN = Environment.GetEnvironmentVariable("GITHUB_API_TOKEN"); + + private IAPIRequestContext Request = null; + + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // 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, + }); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await Request.DisposeAsync(); + } + } +} +``` + +### Write tests + +Now that we initialized request object we can add a few tests that will create new issues in the repository. +```csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; +using NUnit.Framework; + +namespace Playwright.TestingHarnessTest.NUnit +{ + + 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; + + [Test] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary(); + data.Add("title", "[Bug] report 1"); + data.Add("body", "Bug description"); + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + Assert.True(newIssue.Ok); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + Assert.True(issues.Ok); + 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.NotNull(issue); + Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + } + + [Test] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary(); + data.Add("title", "[Feature] request 1"); + data.Add("body", "Feature description"); + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + Assert.True(newIssue.Ok); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + Assert.True(issues.Ok); + var issuesJsonResponse = await issues.JsonAsync(); + var issuesJson = (await issues.JsonAsync())?.EnumerateArray(); + + 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.NotNull(issue); + Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + } + + // ... + } +} +``` + +### Setup and teardown + +These tests assume that repository exists. You probably want to create a new one before running tests and delete it afterwards. Use `[SetUp]` and `[TearDown]` hooks for that. + +```csharp + // ... + + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + Assert.True(resp.Ok); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + Assert.True(resp.Ok); + } +``` + +### Complete test example + +Here is the complete example of an API test: + +```csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.Playwright.NUnit; +using Microsoft.Playwright; +using NUnit.Framework; + +namespace Playwright.TestingHarnessTest.NUnit +{ + + 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; + + [Test] + public async Task ShouldCreateBugReport() + { + var data = new Dictionary(); + data.Add("title", "[Bug] report 1"); + data.Add("body", "Bug description"); + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + Assert.True(newIssue.Ok); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + Assert.True(issues.Ok); + 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.NotNull(issue); + Assert.AreEqual("Bug description", issue?.GetProperty("body").GetString()); + } + + [Test] + public async Task ShouldCreateFeatureRequests() + { + var data = new Dictionary(); + data.Add("title", "[Feature] request 1"); + data.Add("body", "Feature description"); + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + Assert.True(newIssue.Ok); + + var issues = await Request.GetAsync("/repos/" + USER + "/" + REPO + "/issues"); + Assert.True(issues.Ok); + var issuesJsonResponse = await issues.JsonAsync(); + var issuesJson = (await issues.JsonAsync())?.EnumerateArray(); + + 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.NotNull(issue); + Assert.AreEqual("Feature description", issue?.GetProperty("body").GetString()); + } + + [SetUp] + public async Task SetUpAPITesting() + { + await CreateAPIRequestContext(); + await CreateTestRepository(); + } + + private async Task CreateAPIRequestContext() + { + var headers = new Dictionary(); + // 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, + }); + } + + private async Task CreateTestRepository() + { + var resp = await Request.PostAsync("/user/repos", new() + { + DataObject = new Dictionary() + { + ["name"] = REPO, + }, + }); + Assert.True(resp.Ok); + } + + [TearDown] + public async Task TearDownAPITesting() + { + await DeleteTestRepository(); + await Request.DisposeAsync(); + } + + private async Task DeleteTestRepository() + { + var resp = await Request.DeleteAsync("/repos/" + USER + "/" + REPO); + Assert.True(resp.Ok); + } + } +} +``` + +## Prepare server state via API calls + +The following test creates a new issue via API and then navigates to the list of all issues in the +project to check that it appears at the top of the list. The check is performed using [LocatorAssertions]. + +```csharp + [Test] + public async Task LastCreatedIssueShouldBeFirstInTheList() + { + var data = new Dictionary(); + data.Add("title", "[Feature] request 1"); + data.Add("body", "Feature description"); + var newIssue = await Request.PostAsync("/repos/" + USER + "/" + REPO + "/issues", new() { DataObject = data }); + Assert.True(newIssue.Ok); + + // 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"); + } +``` + +## Check the server state after running user actions + +The following test creates a new issue via user interface in the browser and then checks via API if +it was created: + +```csharp + [Test] + public async Task LastCreatedIssueShouldBeOnTheServer() + { + await Page.GotoAsync("https://github.com/" + USER + "/" + REPO + "/issues"); + await Page.ClickAsync("text=New Issue"); + await Page.FillAsync("[aria-label='Title']", "Bug report 1"); + await Page.FillAsync("[aria-label='Comment body']", "Bug description"); + await Page.ClickAsync("text=Submit new issue"); + String issueId = Page.Url.Substring(Page.Url.LastIndexOf('/')); + + var newIssue = await Request.GetAsync("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId); + Assert.True(newIssue.Ok); + StringAssert.Contains(await newIssue.TextAsync(), "Bug report 1"); + } +``` + +## Reuse authentication state + +Web apps use cookie-based or token-based authentication, where authenticated +state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). +Playwright provides [`method: APIRequestContext.storageState`] method that can be used to +retrieve storage state from an authenticated context and then create new contexts with that state. + +Storage state is interchangeable between [BrowserContext] and [APIRequestContext]. You can +use it to log in via API calls and then create a new context with cookies already there. +The following code snippet retrieves state from an authenticated [APIRequestContext] and +creates a new [BrowserContext] with that state. + +```csharp +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 }); +``` diff --git a/docs/src/api-testing-java.md b/docs/src/api-testing-java.md index f30da79674..ad1598cff4 100644 --- a/docs/src/api-testing-java.md +++ b/docs/src/api-testing-java.md @@ -6,7 +6,7 @@ title: "API testing" Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of your application. -Sometimes you may want to send requests to the server directly from Node.js without loading a page and running js code in it. +Sometimes you may want to send requests to the server directly from Java without loading a page and running js code in it. A few examples where it may come in handy: - Test your server API. - Prepare server side state before visiting the web application in a test. diff --git a/docs/src/api-testing-python.md b/docs/src/api-testing-python.md index 0e5d74f616..26d8c70609 100644 --- a/docs/src/api-testing-python.md +++ b/docs/src/api-testing-python.md @@ -6,7 +6,7 @@ title: "API testing" Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) API of your application. -Sometimes you may want to send requests to the server directly from Node.js without loading a page and running js code in it. +Sometimes you may want to send requests to the server directly from Python without loading a page and running js code in it. A few examples where it may come in handy: - Test your server API. - Prepare server side state before visiting the web application in a test.