Mastering API Testing with Playwright: A Comprehensive Guide

Software Testing

Explore Playwright's robust API testing capabilities. Learn to set up APIRequestContext, persist authentication states, and integrate with CircleCI for efficient CI/CD workflows, combining UI and API tests for faster, more reliable end-to-end testing.

API Testing with Playwright

What You'll Learn

This tutorial covers:

  • Prerequisites
  • Playwright’s APIRequestContext
  • Setting up Playwright’s APIRequestContext
  • Sending API requests
  • Persisting Authenticated State
  • Setting up a project on CircleCI

The Playwright framework is widely recognized for its powerful UI testing features, but it also provides robust tools for API testing. Leveraging Playwright's API testing capabilities allows you to build faster and more reliable end-to-end tests. By combining both UI and API tests within a single test suite, you can significantly enhance the efficiency of your testing efforts.

In this tutorial, you will learn how to set up Playwright’s APIRequestContext, manage authentication persistence, and integrate CircleCI to ensure your tests can run effectively within a CI/CD platform.

Prerequisites

Before you begin, ensure you have the following:

  • Node.js (version 16 or later) installed on your machine.
  • CircleCI account: You can sign up for a free CircleCI account if you don’t have one.
  • GitHub account: Your project will need to be hosted on GitHub to connect it with CircleCI.
  • Git installed on your machine.
  • Basic knowledge of Playwright: Familiarity with writing fundamental Playwright tests will be beneficial.
  • Familiarity with CI/CD: A basic understanding of continuous integration and delivery concepts will be helpful.

Start by cloning the GitHub repository from here.

Playwright’s APIRequestContext

End-to-End (E2E) testing alone doesn't always provide the complete testing picture. For many applications, user flows can often be tested more efficiently at the API level. A prime example is managing authentication for every test within a login and signup module. Instead of manually filling in details on both signup and login forms, you can directly call the signup API to create a user and then use that newly created user to log into the system via the API. This approach is not only faster but also more robust and less prone to flakiness.

Playwright’s APIRequestContext bridges the gap between API requirements and the needs of Playwright users. It is a built-in module within the Playwright test framework that enables you to send HTTP(S) requests to a server. It functions as a lightweight version of popular libraries like axios or fetch, but with additional features. Its integration directly into Playwright ensures consistency between your UI and API tests.

This diagram illustrates the role of APIRequestContext in the context of API calls within Playwright.

APIRequestContext also shares the same context with the browser. This powerful feature allows you to share storage sessions and cookies between your UI and API tests, making it exceptionally easy to manage states and authentication within your test suite.

For instance, you can make an API call to log in a user and then open a browser page that is already authenticated using the same session. With APIRequestContext, you can also leverage auto-retries and other Playwright helpers, extending the same functionality used in browser automation tests.

This capability not only allows you to create robust API tests but also expands the utility of your test suite. You can build highly efficient test suites by using APIs to set up the initial state of your tests and then employing browser automation to test the UI components of your application.

Now that you understand APIRequestContext, let's proceed with setting it up in Playwright.

Setting up Playwright’s APIRequestContext

Setting up APIRequestContext begins in the playwright.config.ts file by defining a baseURL for all API requests. This ensures that endpoints can be called without repeatedly declaring a baseURL in individual tests. You can override this variable in specific tests if they need to run against a different base URL.

The following code snippet demonstrates a configuration for playwright.config.ts for a simple application with endpoints under https://api-testing-with-playwright-b1gd.vercel.app/:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // All API requests will be prefixed with this baseURL
    baseURL: 'https://api-testing-with-playwright-b1gd.vercel.app',
    // If you want to run tests against local server you can use 
    // baseURL:localhost:3000
    // or any other port where your application is running
    extraHTTPHeaders: {
      // We can add headers that are common for all requests.
      // For example, an authorization token.
      'Authorization': `Bearer ${process.env.API_TOKEN}`,
      'Content-Type': 'application/json',
    },
  },
});

This snippet can be found in the root of the playwright-tests directory of the cloned repository.

Here’s a breakdown of this configuration:

  • baseURL: Specifies the base URL for all API requests made using the request fixture. For instance, if you make a request to the /users endpoint, Playwright will prepend the baseURL, resulting in a call to https://api-testing-with-playwright-b1gd.vercel.app/users.
  • extraHTTPHeaders: Allows you to define headers that will be sent with every API request. This is an ideal place for static tokens or Content-Type declarations. The ${process.env.API_TOKEN} is a placeholder demonstrating how to use Bearer tokens for authentication, though it's not strictly required in this specific example.

Sending API Requests

Once APIRequestContext is set up, you can start making API requests directly in your test files. Playwright provides a request fixture that you can use to send various HTTP requests, including GET, POST, PUT, DELETE, and others.

Here’s a snippet from a test file that uses the request fixture to fetch a list of users and create a new one. This snippet can be found in the playwright-tests/tests directory.

// tests/api.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Users API', () => {
  // Test to get all users
  test('should be able to get a list of users', async ({ request }) => {
    const response = await request.get('/users');

    // Assert that the request was successful
    expect(response.ok()).toBeTruthy();

    // Assert that the response body is an array
    const responseBody = await response.json();
    expect(Array.isArray(responseBody)).toBeTruthy();
  });

  // Test to create a new user
  test('should be able to create a new user', async ({ request }) => {
    const newUser = {
      name: 'John Doe',
      email: `john.doe.${Date.now()}@example.com`,
    };

    const response = await request.post('/users', {
      data: newUser,
    });

    // Assert that the request was successful (e.g., 201 Created)
    expect(response.status()).toBe(201);
    expect(response.ok()).toBeTruthy();

    // Assert that the response contains the created user's data
    const responseBody = await response.json();
    expect(responseBody.name).toBe(newUser.name);
    expect(responseBody.email).toBe(newUser.email);
    expect(responseBody.id).toBeDefined();
  });
});

In the example above, you can see how intuitive it is to work with the request fixture. You have direct access to functions like request.get() and request.post() for making your API calls. You can then use Playwright’s expect assertions on the response object to verify the status code (response.ok(), response.status()) and the response body (response.json()).

This method provides a very powerful way to test your API’s core functionality in isolation. Testing your APIs this way ensures that your backend behaves as expected even before you introduce UI interactions through browser tests.

Persisting Authenticated State

Building on the concept of combining API and UI tests with a shared state, Playwright also allows for persisting authenticated states across your test suite. This means you can log in using your API endpoints and then use that authenticated user's session in subsequent UI tests. Here are the steps to achieve this:

  1. Make a POST request to your login endpoint.
  2. The server responds with an authentication token, typically in a cookie or within the response body.
  3. Save this authentication state (cookies, local storage) to a file using request.storageState().
  4. Configure your browser tests to utilize this saved authentication state.

Using the cloned repository, examine the playwright-tests/global.setup.ts file to review the authentication process:

// global.setup.ts
import { test as setup, expect } from '@playwright/test';

const authFile = "./userAuth.json";

setup('authenticate', async ({ request }) => {
  // Send a request to log in the user.
  await request.post('/api/login', {
    data: {
      username: 'testUsername',
      password: 'testP@ssword',
    },
  });

  // Save the storage state to the userAuth.json.
  await request.storageState({ path: authFile });
});

This setup step will run once before all tests. Upon successful login, it stores the authentication token in the userAuth.json file. You can then access this token in your subsequent tests without needing to log in every single time.

After successfully logging in, Playwright needs to know how to use this setup file. You can configure this in playwright-tests/playwright.config.ts, as shown below:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  //... other configs

  // Use a global setup file for authentication
  globalSetup: require.resolve('./global.setup.ts'),

  use: {
    //... other use options

    // Use the saved authentication state for all tests
    storageState: 'playwright/auth/userAuth.json',
  },
});

Now, when all UI tests are executed, Playwright will first load all cookies and local storage items from the userAuth.json file, thereby executing your tests with an already authenticated session.

Tip: It's crucial to consider the duration of your cookie session configuration. If a timeout occurs, Playwright needs to be aware of when to re-authenticate the user and verify that the cookie remains active throughout the test run. Failure to account for this can lead to test failures due to expired authentication.

The snippet below shows a UI test that benefits from the configuration above:

// playwright-tests/tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';

test('user can access their dashboard', async ({ page }) => {
  // The user is already logged in thanks to global setup! :)
  await page.goto('/dashboard');

  // Assert that the user's name is visible on the page
  await expect(page.locator('h1')).toContainText('Welcome, Test User!');
});

This state persistence approach not only makes your tests faster but also decouples your UI tests from the login flow, rendering them more focused and less brittle. With authentication state persistence mastered, you can now proceed to run your tests in a CI/CD environment using CircleCI.

Setting up Playwright Tests on CircleCI

If you cloned the provided sample project repository, the CircleCI integration is already in place. If you are integrating Playwright into a new project, follow these steps to set up CircleCI:

First, create a .circleci/config.yml file in the root directory of your project. This file will define the steps to check out your code, install dependencies, and run your Playwright tests. Here is a sample config.yml file designed for running Playwright tests:

version: 2.1
orbs:
  circleci-cli: circleci/circleci-cli@0.1.9
jobs:
  build:
    docker:
      - image: mcr.microsoft.com/playwright:v1.52.0-jammy
    working_directory: ~/repo
    steps:
      - checkout
      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

      - run: cd playwright-tests && npm install
      - run: cd playwright-tests && npx playwright install
      - run: cd playwright-tests && npm run test

      - store_artifacts:
          path: ./playwright-report
          destination: playwright-report-first

Here’s a breakdown of this configuration file:

  • version: 2.1: Specifies the CircleCI configuration version.
  • orbs: Reusable packages of CircleCI configuration that can simplify common tasks. Here, the circleci-cli orb is included.
  • jobs: Defines the tasks to be executed. Here, you have one job named build.
  • steps: Individual commands to be run within a job.
    • checkout: Fetches your source code from the repository.
    • restore_cache: Restores cached dependencies to speed up builds.
    • npm install: Installs your project’s Node.js dependencies.
    • npx playwright install: Downloads the necessary browser binaries that Playwright requires to run tests.
    • npm run test: Executes your Playwright tests.
    • store_artifacts: Saves the Playwright HTML report, which can be viewed in the Artifacts tab of your CircleCI job.

Using this configuration, every time you push a change to your GitHub repository, CircleCI will trigger a build, install all necessary components, and run your full suite of UI and API tests.

Integrating Your Project with CircleCI

If you started this project from scratch (instead of cloning the sample repository), you’ll need to initialize a GitHub repository for your project by running:

git init

Create a .gitignore file in the root directory and add node_modules to it to prevent npm-generated modules from being added to your remote repository. After committing your changes, push your project to GitHub.

Log into CircleCI and navigate to Projects. All repositories associated with your GitHub username or organization will be listed. For this project, locate api-testing-with-playwright.

Click the Set Up Project button and follow the prompts. You will be asked if you have already defined the configuration file (.circleci/config.yml) for CircleCI within your project. Enter the branch name (for this tutorial, we are using main).

After completing the setup, your project build will commence. In this case, the build was successfully completed.

Conclusion

Congratulations! You have successfully learned how to leverage the full potential of Playwright by combining both API and UI testing to write efficient and robust tests. This approach not only ensures the maintainability of your tests but also promotes a clear separation of concerns between browser-based and API-based tests. To recap, you learned:

  • How to use APIRequestContext to send requests and its importance.
  • How to persist authentication state to bridge the gap between your API and UI tests in Playwright.
  • How to automate the entire testing process in CI using CircleCI pipelines.

We hope you found this tutorial enjoyable and informative. Happy testing!