--- 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 Node.js 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. ## 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. You can either put them in the configuration file, or in the test file with `test.use()`. ```js js-flavor=ts // playwright.config.ts import { PlaywrightTestConfig } from '@playwright/test'; const config: PlaywrightTestConfig = { use: { // All requests we send go to this API endpoint. baseURL: 'https://api.github.com', extraHTTPHeaders: { // 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 ${process.env.API_TOKEN}`, }, } }; export default config; ``` ```js js-flavor=js // playwright.config.js // @ts-check /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { use: { // All requests we send go to this API endpoint. baseURL: 'https://api.github.com', extraHTTPHeaders: { // 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 ${process.env.API_TOKEN}`, }, } }; module.exports = config; ``` ### Write tests Playwright Test comes with the built-in `request` fixture that respects configuration options like `baseURL` or `extraHTTPHeaders` we specified and is ready to send some requests. Now we can add a few tests that will create new issues in the repository. ```js const REPO = 'test-repo-1'; const USER = 'github-username'; test('should create a bug report', async ({ request }) => { const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, { data: { title: '[Bug] report 1', body: 'Bug description', } }); expect(newIssue.ok()).toBeTruthy(); const issues = await request.get(`/repos/${USER}/${REPO}/issues`); expect(issues.ok()).toBeTruthy(); expect(await issues.json()).toContainEqual(expect.objectContaining({ title: '[Bug] report 1', body: 'Bug description' })); }); test('should create a feature request', async ({ request }) => { const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, { data: { title: '[Feature] request 1', body: 'Feature description', } }); expect(newIssue.ok()).toBeTruthy(); const issues = await request.get(`/repos/${USER}/${REPO}/issues`); expect(issues.ok()).toBeTruthy(); expect(await issues.json()).toContainEqual(expect.objectContaining({ title: '[Feature] request 1', body: 'Feature description' })); }); ``` ### 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 `beforeAll` and `afterAll` hooks for that. ```js test.beforeAll(async ({ request }) => { // Create a new repository const response = await request.post('/user/repos', { data: { name: REPO } }); expect(response.ok()).toBeTruthy(); }); test.afterAll(async ({ request }) => { // Delete the repository const response = await request.delete(`/repos/${USER}/${REPO}`); expect(response.ok()).toBeTruthy(); }); ``` ### Using request context Behind the scenes, `request` fixture will actually call [`method: APIRequest.newContext`]. You can always do that manually if you'd like more control. Below is a standalone script that does the same as `beforeAll` and `afterAll` from above. ```js const { request } = require('@playwright/test'); const REPO = 'test-repo-1'; const USER = 'github-username'; (async () => { // Create a context that will issue http requests. const context = await request.newContext({ baseURL: 'https://api.github.com', }); // Create a repository. await context.post('/user/repos', { headers: { 'Accept': 'application/vnd.github.v3+json', // Add GitHub personal access token. 'Authorization': `token ${process.env.API_TOKEN}`, }, data: { name: REPO } }); // Delete a repository. await context.delete(`/repos/${USER}/${REPO}`{ headers: { 'Accept': 'application/vnd.github.v3+json', // Add GitHub personal access token. 'Authorization': `token ${process.env.API_TOKEN}`, } }); })() ``` ## 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. ```js test('last created issue should be first in the list', async ({ page, request }) => { const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, { data: { title: '[Feature] request 1', } }); expect(newIssue.ok()).toBeTruthy(); await page.goto(`https://github.com/${USER}/${REPO}/issues`); const firstIssue = page.locator(`a[data-hovercard-type='issue']`).first(); await expect(firstIssue).toHaveText('[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 uses checks if it was created via API: ```js test('last created issue should be on the server', async ({ page, request }) => { await page.goto(`https://github.com/${USER}/${REPO}/issues`); await page.click('text=New Issue'); await page.fill('[aria-label="Title"]', 'Bug report 1'); await page.fill('[aria-label="Comment body"]', 'Bug description'); await page.click('text=Submit new issue'); const issueId = page.url().substr(page.url().lastIndexOf('/')); const newIssue = await request.get(`https://api.github.com/repos/${USER}/${REPO}/issues/${issueId}`); expect(newIssue.ok()).toBeTruthy(); expect(newIssue).toEqual(expect.objectContaining({ title: 'Bug report 1' })); }); ``` ### API reference - [`property: Playwright.request`] - [`property: BrowserContext.request`] - [`property: Page.request`] - [`method: APIRequest.newContext`] - [`method: APIRequestContext.delete`] - [`method: APIRequestContext.fetch`] - [`method: APIRequestContext.get`] - [`method: APIRequestContext.post`] ## 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. ```js const requestContext = await request.newContext({ httpCredentials: { username: 'user', password: 'passwd' } }); await requestContext.get(`https://api.example.com/login`); // Save storage state into the file. await requestContext.storageState({ path: 'state.json' }); // Create a new context with the saved storage state. const context = await browser.newContext({ storageState: 'state.json' }); ``` ### API reference - [`method: Browser.newContext`] - [`method: APIRequestContext.storageState`] - [`method: APIRequest.newContext`]