Skip to main content

API testing

Introduction

Playwright can be used to get access to the REST 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 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. 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.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Playwright.NUnit;
using Microsoft.Playwright;
using NUnit.Framework;

namespace PlaywrightTests;

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<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,
});
}

[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.

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 PlaywrightTests;

[TestFixture]
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<string, string>();
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<string, string>();
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.

public class TestGitHubAPI : PlaywrightTest
{
// ...

[SetUp]
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,
},
});
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:

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 PlaywrightTests;

[TestFixture]
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<string, string>();
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<string, string>();
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<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,
});
}

private async Task CreateTestRepository()
{
var resp = await Request.PostAsync("/user/repos", new()
{
DataObject = new Dictionary<string, string>()
{
["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.

class TestGitHubAPI : PageTest
{
[Test]
public async Task LastCreatedIssueShouldBeFirstInTheList()
{
var data = new Dictionary<string, string>();
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:

class GitHubTests : PageTest
{
[Test]
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();
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. Playwright provides ApiRequestContext.StorageStateAsync() 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.

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 });