End-to-end Testing
How-tos & Guides
9 min read

Using Playwright for API testing

A walkthrough on how to use Playwright to create standalone API tests as well as incorporate API calls into a larger end-to-end testing workflow.

Taofiq Aiyelabegan
Published September 27, 2022
AI Assistant for Playwright
Code AI-powered test steps with the free ZeroStep JavaScript library
Learn more

Starting in version 1.16, Playwright introduced the APIRequestContext class which allows users to make API calls and create assertions for various aspects of the HTTP response. API calls in Playwright are flexible: they are made outside of the browser runtime but can optionally share the cookie state of the browser runtime. This means that you can create both standalone API tests in Playwright, as well as make API calls intermingled with UI actions like clicks and inputs. There are two situations in particular where you might want to incorporate API testing as part of an end-to-end test:

Let’s explore Playwright’s API testing capabilities by creating a sample project to test the Go REST API, which contains a set of API endpoints which were developed for testing purposes and are publicly accessible.

Getting Started

To get started, open your terminal application, create a new directory called playwright-api-testing, and within that directory run the following command::

1
npm init playwright@latest

After answering a few questions in the subsequent prompts, the command will complete and the following directories and files will be created:

1
2
3
4
5
6
7
- tests/
-- example.spec.ts
- tests-examples/
-- demo-todo-app.spec.ts
- package.json
- package-lock.json
- playwright.config.ts

To run our Playwright tests, we will use the command: npx playwright test. Once the tests have finished, we’ll use the following command to view the test results: npx playwright show-report. The show-report command generates a full report of our tests, and includes the ability to filter the test results based on browsers as well as filter on pass/fail status.

Configuring Playwright for API testing

The playwright.config.js file is where all the configuration for our project can be done. Below is the default config file that will be created via the npm init playwright command:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
// playwright.config.ts

import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * See https://playwright.dev/docs/test-configuration.
 */
const config: PlaywrightTestConfig = {
  testDir: "./tests",
  /* Maximum time one test can run for. */
  timeout: 30 * 1000,
  expect: {
    /**
     * Maximum time expect() should wait for the condition to be met.
     * For example in `await expect(locator).toHaveText();`
     */
    timeout: 5000,
  },
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: "html",
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
    actionTimeout: 0,
    /* Base URL to use in actions like `await page.goto('/')`. */
    // baseURL: 'http://localhost:3000',
    baseURL: "https://gorest.co.in/",

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: "on-first-retry",
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
      },
    },

    // {
    //   name: "firefox",
    //   use: {
    //     ...devices["Desktop Firefox"]
    //   }
    // },

    // {
    //   name: "webkit",
    //   use: {
    //     ...devices["Desktop Safari"]
    //   }
    // }

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: {
    //     ...devices['Pixel 5'],
    //   },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: {
    //     ...devices['iPhone 12'],
    //   },
    // },

    /* Test against branded browsers. */
    // {
    //   name: 'Microsoft Edge',
    //   use: {
    //     channel: 'msedge',
    //   },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: {
    //     channel: 'chrome',
    //   },
    // },
  ],

  /* Folder for test artifacts such as screenshots, videos, traces, etc. */
  // outputDir: 'test-results/',

  /* Run your local dev server before starting the tests */
  // webServer: {
  //   command: 'npm run start',
  //   port: 3000,
  // },
};

export default config;

The Go REST API that we’ll be testing against requires an access token to be passed in all HTTP requests. We can configure Playwright to pass the access token in all requests by defining the baseURL and extraHTTPHeaders properties in the config file as shown in the code below:

1
2
3
4
5
6
7
8
9
use: {
  ...
  baseURL: "https://gorest.co.in/",
   extraHTTPHeaders: {
     Authorization:
       "Bearer 261f81de55c567c500a4fba73192f46978081fcf96d64cca539616ed5449c730"
   },
  ...
}

Making API calls

The examples below use Playwright’s built-in request function, which is an instance of APIRequestContext, to issue HTTP requests and to access different parts of the HTTP response. As with most of Playwright’s built-in functions, the request function returns a Promise which makes it easy to use with JavaScript’s async/await constructs.

POST example

To create a new user we’ll use a POST request to the /v2/users endpoint, passing all required fields in the request body. Specifically, the name, gender, status, and email properties are required. Since the status of our response should be 201, we can set an expected condition by invoking expect and accessing the response code via the response.status() method. Also, we can check if we are getting an OK response back when the API call is made. Finally, we can print the response we are getting back to the console (although I would recommend removing this once you’ve verified your test is working correctly).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
test("create a new user", async ({ request, baseURL }) => {
  const response = await request.post(`${baseURL}public/v2/users`, {
    data: {
      name: "Taofiq",
      email: "taofiq@outlook.com",
      status: "inactive",
      gender: "Male",
    },
  });

  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(201);
  console.log(await response.json());
});

After running the code above, you should see the test run successfully as shown below:

To get the HTML report run we can run the command npx playwright show-report:

GET example

To get or read the users that have already been created, we’ll call the request.get() method, passing in the endpoint that returns the collection of users. Similar to the example above, we’ll assert against some of the aspects of the HTTP response and then log the response in a temporary console.log() statement.

1
2
3
4
5
6
test("Get users", async ({ request, baseURL }) => {
  const _response = await request.get(`${baseURL}public/v2/users/`);
  expect(_response.ok()).toBeTruthy();
  expect(_response.status()).toBe(200);
  console.log(await _response.json());
});

After running the test, you should see output similar to the screenshot below:

Passing query parameters

We might have cases where we need to pass one or more URL parameters in our API call. In the example below, we call the same /v2/users/ endpoint as in the previous example, but pass the id query parameter so that only that user’s data is returned (if it exists).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
test("Get one user", async ({ request, baseURL }) => {
  const _response = await request.get(`${baseURL}public/v2/users/`, {
    params: {
      id: 5229,
    },
  });
  expect(_response.ok()).toBeTruthy();
  expect(_response.status()).toBe(200);
  console.log(await _response.json());
});

The output from running the test is shown below:

Updating a resource (PUT)

In most APIs, PUT will be used in order to update an existing resource. In the example below, we want to change the user with the name: Taofiqq and email: taofiq@hotmail.com to a name called Zambo. We will pass the name in the request body via the data property, run the expected checks, and log the response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
test("Update a user", async ({ request, baseURL }) => {
  const response = await request.put(`${baseURL}public/v2/users/5721`, {
    data: {
      name: "Zambo",
    },
  });
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);
  console.log(await response.json());
});

The response:

Deleting a resource

In our final example, we’ll invoke a DELETE endpoint to delete a user. Note that in the example below, we’re deleting a hard-coded user.

A status check is done for the status code returned by the delete method, which is expected to be 204. The 204 “No Content” status code is what is returned by Go REST when the user has been successfully deleted:

1
2
3
4
5
test("Delete a user", async ({ request, baseURL }) => {
  const response = await request.delete(`${baseURL}public/v2/users/5217`);
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(204);
});

We can query the “get all users” endpoint to check if that user has successfully deleted.

As we can see, the user taofiq@outlook.com has been deleted and was not returned.

Note that to make this test more resilient, we’d instead recommend creating the user first, grabbing the id of the newly created user, and then calling the DELETE endpoint to delete the user. This pattern of creating and then immediately deleting the new resource is a great way to reduce the amount of pre-existing state your tests depend upon, and thus ensure your tests are less flaky than if you were deleting a pre-existing resource that was created before the tests ran.

Conclusion

In this article, we covered how to use Playwright’s request function to either create standalone API tests, or to incorporate API calls and assertions as part of a larger end-to-end test. In the examples provided above, we demonstrated how to create Playwright tests for the four most popular HTTP verbs: GET, POST, PUT, and DELETE.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © Reflect Software Inc. All Rights Reserved.