docs: update API testing doc (#9561)
This commit is contained in:
parent
d2a0763958
commit
4364c5f248
|
|
@ -7,86 +7,79 @@ Playwright can be used to get access to the [REST](https://en.wikipedia.org/wiki
|
||||||
your application.
|
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 Node.js without loading a page and running js code in it.
|
||||||
A few example where it may come in handy:
|
A few examples where it may come in handy:
|
||||||
- Test your server API.
|
- Test your server API.
|
||||||
- Prepare server side state before visting the web application in a test.
|
- Prepare server side state before visiting the web application in a test.
|
||||||
- Validate server side post-conditions after running some actions in the browser
|
- Validate server side post-conditions after running some actions in the browser.
|
||||||
All of that could be achived via [ApiRequestContext] methods.
|
|
||||||
|
All of that could be achieved via [ApiRequestContext] methods.
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
|
|
||||||
## Writing API Test
|
## Writing API Test
|
||||||
|
|
||||||
The Playwright [ApiRequestContext] can send all kinds of HTTP(S) requests over network.
|
[ApiRequestContext] can send all kinds of HTTP(S) requests over network.
|
||||||
|
|
||||||
The following example demonstrates how to use Plawright to test programmatic creation of
|
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:
|
||||||
issues 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 new repo before all tests
|
- Create a few issues and validate server state.
|
||||||
- create a few issues and validate the server state
|
- Delete the repository after running tests.
|
||||||
- after all tests finish delete the repo
|
|
||||||
|
|
||||||
Creating and deleting a repo:
|
### Configure
|
||||||
```js
|
|
||||||
const { request } = require('@playwright/test');
|
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()`.
|
||||||
...
|
|
||||||
const context = await request.newContext();
|
```js js-flavor=ts
|
||||||
await context.post('https://api.github.com/user/repos', {
|
// playwright.config.ts
|
||||||
headers: {
|
import { PlaywrightTestConfig } from '@playwright/test';
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
// Add GitHub personal access token.
|
const config: PlaywrightTestConfig = {
|
||||||
'Authorization': `token ${process.env.API_TOKEN}`,
|
use: {
|
||||||
},
|
// All requests we send go to this API endpoint.
|
||||||
data: {
|
baseURL: 'https://api.github.com',
|
||||||
name: 'test-repo-1'
|
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}`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
const response = await request.delete(`https://api.github.com/repos/${user}/test-repo-1`{
|
export default config;
|
||||||
headers: {
|
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
|
||||||
// Add GitHub personal access token.
|
|
||||||
'Authorization': `token ${process.env.API_TOKEN}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Playwright Test comes with a built in request fixture that can be used to simplify the code. Also since
|
```js js-flavor=js
|
||||||
the authorizaztion token is going to be reused between tests it makes sense to configure it once for
|
// playwright.config.js
|
||||||
all tests:
|
// @ts-check
|
||||||
|
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||||
```js
|
const config = {
|
||||||
test.use({
|
use: {
|
||||||
baseURL: 'https://api.github.com',
|
// All requests we send go to this API endpoint.
|
||||||
extraHTTPHeaders: {
|
baseURL: 'https://api.github.com',
|
||||||
'Accept': 'application/vnd.github.v3+json',
|
extraHTTPHeaders: {
|
||||||
// Add authorization token to all requests.
|
// We set this header per GitHub guidelines.
|
||||||
'Authorization': 'token ' + token,
|
'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;
|
||||||
const repo = 'test-repo-1';
|
|
||||||
|
|
||||||
// The request object will use the context parameters above.
|
|
||||||
test.beforeAll(async ({ request }) => {
|
|
||||||
// Create 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();
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Now we can add a couple tests that would create new issues in the repository:
|
### 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
|
```js
|
||||||
test('should create bug report', async ({ request }) => {
|
const REPO = 'test-repo-1';
|
||||||
const newIssue = await request.post(`/repos/${user}/${repo}/issues`, {
|
const USER = 'github-username';
|
||||||
|
|
||||||
|
test('should create a bug report', async ({ request }) => {
|
||||||
|
const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, {
|
||||||
data: {
|
data: {
|
||||||
title: '[Bug] report 1',
|
title: '[Bug] report 1',
|
||||||
body: 'Bug description',
|
body: 'Bug description',
|
||||||
|
|
@ -94,7 +87,7 @@ test('should create bug report', async ({ request }) => {
|
||||||
});
|
});
|
||||||
expect(newIssue.ok()).toBeTruthy();
|
expect(newIssue.ok()).toBeTruthy();
|
||||||
|
|
||||||
const issues = await request.get(`/repos/${user}/${repo}/issues`);
|
const issues = await request.get(`/repos/${USER}/${REPO}/issues`);
|
||||||
expect(issues.ok()).toBeTruthy();
|
expect(issues.ok()).toBeTruthy();
|
||||||
expect(await issues.json()).toContainEqual(expect.objectContaining({
|
expect(await issues.json()).toContainEqual(expect.objectContaining({
|
||||||
title: '[Bug] report 1',
|
title: '[Bug] report 1',
|
||||||
|
|
@ -102,8 +95,8 @@ test('should create bug report', async ({ request }) => {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create feature request', async ({ request }) => {
|
test('should create a feature request', async ({ request }) => {
|
||||||
const newIssue = await request.post(`/repos/${user}/${repo}/issues`, {
|
const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, {
|
||||||
data: {
|
data: {
|
||||||
title: '[Feature] request 1',
|
title: '[Feature] request 1',
|
||||||
body: 'Feature description',
|
body: 'Feature description',
|
||||||
|
|
@ -111,7 +104,7 @@ test('should create feature request', async ({ request }) => {
|
||||||
});
|
});
|
||||||
expect(newIssue.ok()).toBeTruthy();
|
expect(newIssue.ok()).toBeTruthy();
|
||||||
|
|
||||||
const issues = await request.get(`/repos/${user}/${repo}/issues`);
|
const issues = await request.get(`/repos/${USER}/${REPO}/issues`);
|
||||||
expect(issues.ok()).toBeTruthy();
|
expect(issues.ok()).toBeTruthy();
|
||||||
expect(await issues.json()).toContainEqual(expect.objectContaining({
|
expect(await issues.json()).toContainEqual(expect.objectContaining({
|
||||||
title: '[Feature] request 1',
|
title: '[Feature] request 1',
|
||||||
|
|
@ -120,41 +113,101 @@ test('should create feature request', async ({ request }) => {
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Preparing server state via API calls
|
### 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
|
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.
|
project to check that it appears at the top of the list.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
test('last created issue should be first in the list', async ({ page, request }) => {
|
test('last created issue should be first in the list', async ({ page, request }) => {
|
||||||
const newIssue = await request.post(`/repos/${user}/${repo}/issues`, {
|
const newIssue = await request.post(`/repos/${USER}/${REPO}/issues`, {
|
||||||
data: {
|
data: {
|
||||||
title: '[Feature] request 1',
|
title: '[Feature] request 1',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(newIssue.ok()).toBeTruthy();
|
expect(newIssue.ok()).toBeTruthy();
|
||||||
|
|
||||||
await page.goto(`https://github.com/${user}/${repo}/issues`);
|
await page.goto(`https://github.com/${USER}/${REPO}/issues`);
|
||||||
const text = await page.locator(`a[data-hovercard-type='issue']`).first().textContent()
|
const firstIssue = page.locator(`a[data-hovercard-type='issue']`).first();
|
||||||
expect('[Feature] request 1');
|
await expect(firstIssue).toHaveText('[Feature] request 1');
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Checking server state after running user actions
|
## 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
|
The following test creates a new issue via user interface in the browser and then uses checks if
|
||||||
it was created by means of the server API:
|
it was created via API:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
test('last created issue should be on server', async ({ page, request }) => {
|
test('last created issue should be on the server', async ({ page, request }) => {
|
||||||
await page.goto(`https://github.com/${user}/${repo}/issues`);
|
await page.goto(`https://github.com/${USER}/${REPO}/issues`);
|
||||||
await page.click('text=New Issue');
|
await page.click('text=New Issue');
|
||||||
await page.fill('[aria-label="Title"]', 'Bug report 1');
|
await page.fill('[aria-label="Title"]', 'Bug report 1');
|
||||||
await page.fill('[aria-label="Comment body"]', 'Bug description');
|
await page.fill('[aria-label="Comment body"]', 'Bug description');
|
||||||
await page.click('text=Submit new issue');
|
await page.click('text=Submit new issue');
|
||||||
const issueId = page.url().substr(page.url().lastIndexOf('/'));
|
const issueId = page.url().substr(page.url().lastIndexOf('/'));
|
||||||
|
|
||||||
const newIssue = await request.get(`https://api.github.com/repos/${user}/${repo}/issues/${issueId}`);
|
const newIssue = await request.get(`https://api.github.com/repos/${USER}/${REPO}/issues/${issueId}`);
|
||||||
expect(newIssue.ok()).toBeTruthy();
|
expect(newIssue.ok()).toBeTruthy();
|
||||||
expect(newIssue).toEqual(expect.objectContaining({
|
expect(newIssue).toEqual(expect.objectContaining({
|
||||||
title: 'Bug report 1'
|
title: 'Bug report 1'
|
||||||
|
|
@ -175,12 +228,12 @@ test('last created issue should be on server', async ({ page, request }) => {
|
||||||
## Reuse authentication state
|
## Reuse authentication state
|
||||||
|
|
||||||
Web apps use cookie-based or token-based authentication, where authenticated
|
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)
|
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
|
Playwright provides [`method: ApiRequestContext.storageState`] method that can be used to
|
||||||
retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
|
retrieve storage state from an authenticated context and then create new contexts with that state.
|
||||||
|
|
||||||
Storage state is interchangable between [BrowserContext] and [ApiRequestContext]. You can
|
Storage state is interchangeable between [BrowserContext] and [ApiRequestContext]. You can
|
||||||
use it e.g. to log in via API calls and then create a new context with retrived cookies.
|
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
|
The following code snippet retrieves state from an authenticated [ApiRequestContext] and
|
||||||
creates a new [BrowserContext] with that state.
|
creates a new [BrowserContext] with that state.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue