2021-10-08 05:38:13 +03:00
---
2021-10-21 04:38:01 +03:00
id: test-api-testing
2021-10-08 05:38:13 +03:00
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.
2021-10-18 16:31:38 +03:00
A few examples where it may come in handy:
2021-10-08 05:38:13 +03:00
- Test your server API.
2021-10-18 16:31:38 +03:00
- Prepare server side state before visiting the web application in a test.
- Validate server side post-conditions after running some actions in the browser.
2021-10-19 17:38:27 +03:00
All of that could be achieved via [APIRequestContext] methods.
2021-10-08 05:38:13 +03:00
2021-10-21 21:44:06 +03:00
<!-- TOC3 -->
2021-10-08 05:38:13 +03:00
## Writing API Test
2021-10-19 17:38:27 +03:00
[APIRequestContext] can send all kinds of HTTP(S) requests over network.
2021-10-08 05:38:13 +03:00
2021-10-18 16:31:38 +03:00
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.
2021-10-08 05:38:13 +03:00
2021-10-21 21:44:06 +03:00
### Configuration
2021-10-08 05:38:13 +03:00
2021-10-18 16:31:38 +03:00
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()` .
2021-10-08 05:38:13 +03:00
2021-10-18 16:31:38 +03:00
```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}` ,
},
2021-10-08 05:38:13 +03:00
}
2021-10-18 16:31:38 +03:00
};
export default config;
```
2021-10-08 05:38:13 +03:00
2021-10-18 16:31:38 +03:00
```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;
```
2021-10-08 05:38:13 +03:00
2021-10-21 21:44:06 +03:00
### Writing tests
2021-10-08 05:38:13 +03:00
2021-10-18 16:31:38 +03:00
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.
2021-10-08 05:38:13 +03:00
2021-10-18 16:31:38 +03:00
Now we can add a few tests that will create new issues in the repository.
2021-10-08 05:38:13 +03:00
```js
2021-10-18 16:31:38 +03:00
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`, {
2021-10-08 05:38:13 +03:00
data: {
title: '[Bug] report 1',
body: 'Bug description',
}
});
expect(newIssue.ok()).toBeTruthy();
2021-10-18 16:31:38 +03:00
const issues = await request.get(`/repos/${USER}/${REPO}/issues`);
2021-10-08 05:38:13 +03:00
expect(issues.ok()).toBeTruthy();
expect(await issues.json()).toContainEqual(expect.objectContaining({
title: '[Bug] report 1',
body: 'Bug description'
}));
});
2021-10-18 16:31:38 +03:00
test('should create a feature request', async ({ request }) => {
const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, {
2021-10-08 05:38:13 +03:00
data: {
title: '[Feature] request 1',
body: 'Feature description',
}
});
expect(newIssue.ok()).toBeTruthy();
2021-10-18 16:31:38 +03:00
const issues = await request.get(`/repos/${USER}/${REPO}/issues`);
2021-10-08 05:38:13 +03:00
expect(issues.ok()).toBeTruthy();
expect(await issues.json()).toContainEqual(expect.objectContaining({
title: '[Feature] request 1',
body: 'Feature description'
}));
});
```
2021-10-18 16:31:38 +03:00
### 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();
});
```
2021-10-21 21:44:06 +03:00
## Using request context
2021-10-18 16:31:38 +03:00
2021-10-21 21:44:06 +03:00
Behind the scenes, [`request` fixture ](./api/class-fixtures#fixtures-request ) 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.
2021-10-18 16:31:38 +03:00
```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}` ,
}
});
})()
```
2021-10-21 21:44:06 +03:00
## Sending API requests from UI tests
While running tests inside browsers you may want to make calls to the HTTP API of your application. It may be helpful if you need to prepare server state before running a test or to check some postconditions on the server after performing some actions in the browser. All of that could be achieved via [APIRequestContext] methods.
### Establishing preconditions
2021-10-08 05:38:13 +03:00
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.
2021-10-21 21:44:06 +03:00
```js js-flavor=ts
import { test, expect } from '@playwright/test';
const REPO = 'test-repo-1';
const USER = 'github-username';
// Request context is reused by all tests in the file.
let apiContext;
test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
// 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}` ,
},
});
})
test.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});
test('last created issue should be first in the list', async ({ page }) => {
const newIssue = await apiContext.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');
});
```
```js js-flavor=js
// @ts -check
const { test, expect } = require('@playwright/test');
const REPO = 'test-repo-1';
const USER = 'github-username';
// Request context is reused by all tests in the file.
let apiContext;
test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
// 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}` ,
},
});
})
test.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});
test('last created issue should be first in the list', async ({ page }) => {
const newIssue = await apiContext.post(`/repos/${USER}/${REPO}/issues`, {
2021-10-08 05:38:13 +03:00
data: {
title: '[Feature] request 1',
}
});
expect(newIssue.ok()).toBeTruthy();
2021-10-18 16:31:38 +03:00
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');
2021-10-08 05:38:13 +03:00
});
```
2021-10-21 21:44:06 +03:00
### Validating postconditions
2021-10-08 05:38:13 +03:00
The following test creates a new issue via user interface in the browser and then uses checks if
2021-10-18 16:31:38 +03:00
it was created via API:
2021-10-08 05:38:13 +03:00
2021-10-21 21:44:06 +03:00
```js js-flavor=ts
import { test, expect } from '@playwright/test';
const REPO = 'test-repo-1';
const USER = 'github-username';
// Request context is reused by all tests in the file.
let apiContext;
test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
// 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}` ,
},
});
})
test.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});
2021-10-18 16:31:38 +03:00
test('last created issue should be on the server', async ({ page, request }) => {
await page.goto(`https://github.com/${USER}/${REPO}/issues`);
2021-10-08 05:38:13 +03:00
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('/'));
2021-10-18 16:31:38 +03:00
const newIssue = await request.get(`https://api.github.com/repos/${USER}/${REPO}/issues/${issueId}`);
2021-10-08 05:38:13 +03:00
expect(newIssue.ok()).toBeTruthy();
expect(newIssue).toEqual(expect.objectContaining({
title: 'Bug report 1'
}));
});
```
2021-10-21 21:44:06 +03:00
```js js-flavor=js
// @ts -check
const { test, expect } = require('@playwright/test');
const REPO = 'test-repo-1';
const USER = 'github-username';
// Request context is reused by all tests in the file.
let apiContext;
test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
// 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}` ,
},
});
})
test.afterAll(async ({ }) => {
// Dispose all responses.
await apiContext.dispose();
});
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'
}));
});
```
2021-10-08 05:38:13 +03:00
2021-10-21 21:44:06 +03:00
## Reusing authentication state
2021-10-08 05:38:13 +03:00
Web apps use cookie-based or token-based authentication, where authenticated
2021-10-18 16:31:38 +03:00
state is stored as [cookies ](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies ).
2021-10-19 17:38:27 +03:00
Playwright provides [`method: APIRequestContext.storageState`] method that can be used to
2021-10-18 16:31:38 +03:00
retrieve storage state from an authenticated context and then create new contexts with that state.
2021-10-08 05:38:13 +03:00
2021-10-19 17:38:27 +03:00
Storage state is interchangeable between [BrowserContext] and [APIRequestContext]. You can
2021-10-18 16:31:38 +03:00
use it to log in via API calls and then create a new context with cookies already there.
2021-10-19 17:38:27 +03:00
The following code snippet retrieves state from an authenticated [APIRequestContext] and
2021-10-08 05:38:13 +03:00
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' });
```