The Comprehensive Guide to Playwright API Testing

July 15, 2023
10 min

The software development lifecycle (SDLC) outlines a systematic approach to designing, developing, and testing software. Test automation is a key component of this process, offering several advantages: Automating repetitive tasks speeds up testing cycles, minimizes human error, and seamlessly integrates with CI/CD pipelines.

This article provides a comprehensive guide to testing your APIs with Playwright, an end-to-end testing framework. We also include practical examples and best practices to help you start writing automated tests. A basic understanding of JavaScript/TypeScript and NPM is beneficial since Playwright is a Node.js-based tool, but this is not required.

Summary of key Playwright API testing best practices

The following table outlines five best practices for API testing using Playwright and test automation in general. In the subsequent sections, we delve deeper into each best practice, providing specific examples and recommendations for effective and thorough API testing.

Best practice Description
Establish an API testing approach Understand your API structure and draft tests accordingly while accounting for network/proxy considerations.
Establish testing preconditions Assumptions and preconditions should be determined in the early stages of testing at the project and test levels.
Isolate testing environments Keep environments distinct to prevent interference from other tests, such as existing data or configurations.
Mock external network traffic Whenever control over the response data is needed, isolate the web application from the actual API or test the application's behavior even before the actual API is fully developed.
Use CI/CD tools Use build automation to maintain and run tests more frequently. This helps keep test cases relevant.
Use AI testing tools A new generation of AI testing tools can generate Playwright code that you can customize.

{{banner-large-dark-2="/banners"}}

Establish an API testing approach

Understand your API

Before diving into testing, it's crucial to understand your API thoroughly. This includes understanding endpoints, request methods, parameters, response formats, and error codes. The table below lays out some key components, considerations, and actions to keep in mind.

Component Consideration Action
Endpoints Identifying all endpoints Create a comprehensive list of all available API endpoints.
Prioritize endpoints Determine each priority based on usage frequency, business impact, and security implications.
Versioning If your API supports multiple versions, ensure that your tests cover all relevant versions.
HTTP methods Method-specific testing Design test cases that accurately simulate the expected behavior for each HTTP method. For instance, you might write tests to verify that a request is successful if given the correct parameters and unsuccessful if not.
Additional protocol considerations While most API testing focuses on HTTP, it's essential to verify whether your API supports other protocols (e.g., gRPC, WebSockets). If other protocols are in use, ensure that tests cover them. Comprehensive coverage across protocols is a general best practice.
Request and response formats Data structures Define the expected data structures for request and response payloads. Use tools like JSON Schema or OpenAPI to validate the data's correctness.
Data types Ensure that your tests handle different data types (e.g., strings, numbers, booleans, arrays, and objects) correctly.
Error handling Test how the API handles invalid or unexpected data formats and verify that appropriate error messages are returned.
Authentication and authorization Security mechanisms Understand the authentication and authorization mechanisms used by your API (e.g., API keys, OAuth, or basic authentication).
Test security Create test cases that verify the proper functioning of security mechanisms, such as checking that unauthorized users cannot access protected resources.
Token expiration Test how the API handles expired tokens and redirects users to the appropriate authentication flow.
Role-based access control If your API implements role-based access control, test that users with different roles have the correct permissions.

Network and proxy considerations

Once you understand your API, it's time to explore your chosen testing framework/tool. For this article, of course, we’re using Playwright. Let’s set up Playwright with proper network and proxy configurations, such as configuring proxies to test location-dependent services and handling API rate limits. 

Network factors can introduce new requirements that must be considered:

  • If your application uses location-dependent services, such as displaying localized content or recommendations, you must use proxies to simulate different geographic locations to ensure correct functionality.
  • If your application has region-specific access restrictions, you’ll need to use proxies to bypass (or test) these restrictions and test from different locations. Playwright can be configured to use an HTTPS or SOCKS5 proxy, which allows you to route network traffic through a proxy server.
  • Some APIs have a rate limit to prevent abuse. You can simulate multiple users accessing an API by launching multiple Playwright instances and configuring them to use the same proxy.

API-only or mixed testing?

Identifying the appropriate testing tools and methodologies early on allows for more efficient resource allocation and prevents unnecessary delays. Deciding whether to stick to API or mixed testing exclusively is essential. The table below should give you a rough idea of how to proceed.

Feature API only Mixed
Use cases Ideal for microservices architectures where APIs are the primary means of communication between components Ideal for traditional web applications where the API powers the UI
Tools Popular tools include Postman, Insomnia, SoapUI, RestAssured, and Playwright (in API testing mode—see the section below on the Playwright approach) Popular tools include Playwright, Cypress, and Selenium (with API testing capabilities)
Benefits Offers isolation, allowing for more granular testing and faster feedback loops Provides a more holistic view of the application's performance and user experience, ensuring that the API and UI work seamlessly together
Challenges May not fully capture real-world user interactions and potential UI-related issues Setup and maintenance can be more complex, especially for large applications

Establishing testing preconditions

Creating a controlled environment that mirrors real-world scenarios is essential when conducting API testing. This involves determining the in-app state details and then setting up preconditions accordingly. Preconditions ensure that each test starts from a known, stable state—they simplify the debugging process because you know exactly what state the system was in when the test was conducted. This makes it easier to trace failures back to their source.

Deciding on in-app state details

To ensure accurate and meaningful test results, determine the specific in-app state details that should be simulated for each test. This involves identifying the necessary data, configurations, or user states that should be present at the beginning of a test.

Here are a few in-app state details to consider:

  • User roles: Simulating different user roles (e.g., admin, customer, guest) to test access controls and permissions
  • Data existence: Ensuring the presence of specific data entities (e.g., products, orders) in the database
  • Application settings: Configuring application settings (e.g., language, theme) to match specific user preferences
  • Network conditions: Simulating different network conditions (e.g., latency or errors) to test the API's resilience (see the earlier discussion of network and proxy considerations).

Project-level vs. test-level preconditions

Preconditions should be established on two levels: project and test.

Project-level preconditions are applied across multiple test cases and are generally less likely to change frequently. They often involve setting up the testing environment or configuring external dependencies. Here are some examples:

  • Installing required dependencies or libraries
  • Configuring internal/external services, such as database connections, payment gateways, email providers, etc. 
  • Creating test users with predefined roles and permissions
  • Ensuring that data exists in a shopping cart when that endpoint is being tested

Test-level preconditions are tailored to individual test cases and may change frequently. They often involve setting up specific data or simulating user interactions to create the desired initial state. For example:

  • Logging in as a specific user role (e.g., admin, user, custom role, etc.)
  • Creating or updating specific data entities (e.g., products, customers)
  • Simulating network conditions (e.g., an API is only called from mobile browsers, so we mock devices)

{{banner-small-3="/banners"}}

Isolate testing environments

Test isolation ensures that each test operates independently, preventing interference from other tests. Playwright ensures test isolation by running each test in a separate browser context. It's similar to opening a new and separate web browsing session (e.g., an incognito window), providing a clean slate for each set of tests. This prevents cascading failures and enhances test reproducibility. 

Each context has its own independent storage, cookies, and session data, ensuring that tests don't interfere with one another. Playwright efficiently creates a new context for every test and a default page within that context.

The APIRequestContext method allows you to interact with API endpoints, configure microservices, and prepare the environment or service for your end-to-end tests. Every Playwright browser context has an associated APIRequestContext instance that shares cookie storage with the browser context. You can access this instance using browserContext.request or page.request. Additionally, you can create new APIRequestContext instances manually by calling apiRequest.newContext().

In the example below, we iterate through an array of users representing different login scenarios (for example, to test the authentication of different user roles). We create a new context for each user using browser.newContext(), which ensures a clean environment with no existing session data. After testing login success/failure, we close the context using context.close() to release resources.

// Simulate multiple login attempts with different users
async function testLogin() {
  for (const user of users) {
    // Create a new browser context for each user (clean slate)
    const context = await browser.newContext();
    const page = await context.newPage();

    // Login with specific user credentials
    await page.goto('https://app.qualiti.ai/auth/login');
    await page.fill('#username', user.username);
    await page.fill('#password', user.password);
    await page.click('#submit-button');

    // Assert login success/failure based on user credentials
    // ...

    // Close the context after each login attempt
    await context.close();
  }
}

In the other example below, we create a new context but don't close it immediately. We simulate a successful login using predefined credentials to perform actions that require a logged-in user (such as editing a user’s profile) and leverage existing session data.

async function testUserActions() {
  // Assuming login is successful in a previous test
  const context = await browser.newContext();
  const page = await context.newPage();

  // Login to Qualiti using another user
  await page.goto('https://app.qualiti.ai/auth/login');
  await page.fill('#username', 'valid_user');
  await page.fill('#password', 'valid_password');
  await page.click('#submit-button');

  // User is now logged in within the existing context

  // Perform actions requiring a logged-in user (eg., edit profile)
  await page.goto('https://your-app.com/profile');
  await page.click('#edit-profile-button');

  // Close the context when finished
  await context.close();
}

Playwright also provides a powerful mechanism for simulating interactions from multiple users or devices simultaneously within a single test using multiple browser contexts. This is particularly useful for scenarios that require the following:

  • Concurrent user interactions: Testing how the application handles multiple users accessing different parts of the application simultaneously
  • Device compatibility: Verifying that the application works as expected across different browsers and devices
  • User role testing: Simulating interactions from users with different roles or permissions

The following snippet simulates two users interacting with a chat application by creating a separate context for each user:

const { chromium } = require('playwright');

// Create a Chromium browser instance
const browser = await chromium.launch();

// Create two isolated browser contexts
const user1Context = await browser.newContext();
const user2Context = await browser.newContext();

// Create pages for each user
const user1Page = await user1Context.newPage();
const user2Page = await user2Context.newPage();

// Simulate two users interacting with a chat application
async function testChat() {
  // User 1 sends a message
  await user1Page.goto('https://your-chat-app/room');
  await user1Page.fill('#message-input', 'Hello, user 2!');
  await user1Page.click('#send-button');

  // User 2 receives the message and replies
  await user2Page.goto('https://your-chat-app/room');
  await user2Page.waitForSelector('#message-list', { state: 'attached' });
  const message = await user2Page.locator('#message-list').first();
  expect(message.textContent()).toContain('Hello, user 2!');

  await user2Page.fill('#message-input', 'Hi, user 1!');
  await user2Page.click('#send-button');

}

// Run the test
testChat();

These simple examples showcase the benefits of using Playwright's context management for maintaining test isolation and handling different testing scenarios.

Mock external network traffic

While the primary goal of API testing is to verify the functionality of an API, mocking API requests can provide significant advantages in specific scenarios. Here are a few example scenarios where mocking external network traffic is needed.

Granular control over response data:

  • Custom scenarios: Mocking allows you to create precisely tailored scenarios with specific data sets, error responses, or edge cases. This enables you to test your application's behavior thoroughly under various conditions.
  • Data validation: You can provide controlled input to verify that your application correctly handles different data types, formats, and edge cases.
  • Error handling: Test how your application responds to expected and unexpected API errors, ensuring graceful error handling and a good user experience.

Isolation and focus on application logic:

  • Decoupling: Mocking the API isolates your application from the actual API, allowing you to concentrate solely on testing the application's logic and UI rendering.
  • Reduced dependencies: By decoupling your application from the API, you can test independently, reducing dependencies and simplifying the testing process.
  • Faster feedback loops: Mocking can accelerate testing cycles by eliminating the need to wait for the API to be available or respond reliably.

Early development and testing:

  • Parallel development: Mocking APIs can enable parallel front-end and back-end development, allowing you to iterate on the UI and application logic without waiting for the API to be fully functional.
  • Reduced risk: By testing early, you can identify and address potential issues before they become more difficult to resolve.

How to mock external network traffic with Playwright

Playwright allows you to intercept, modify, and mock HTTP and HTTPS network traffic. This includes simulating or altering any requests made by a page, such as XHRs and fetch requests. Additionally, Playwright enables you to mock network interactions using HTTP archive (HAR) files, a standardized format for recording and storing detailed information about network requests and responses. They provide a comprehensive snapshot of all network activity during a web page's loading process.

Let’s look into the three different methods of mocking external network traffic.

Mocking API requests

  • Purpose: Intercepts and replaces actual API requests with custom responses.
  • Use case: Testing API-dependent functionality without relying on the actual API, simulating error scenarios, or controlling response data.
  • Example: This test demonstrates mocking an API request and verifying its impact on the application. We want to intercept all calls to */**/api/v1/fruits and return a custom response containing a single fruit (“Strawberry”) instead of querying the actual API.
test('mocks a fruit without calling the api', async ({ page }) => {
  // Mock the API route before navigation
  await page.route('*/**/api/v1/fruits', async (route) => {
    const mockData = [{ name: 'Strawberry', id: 21 }];
    await route.fulfill({ json: mockData });
  });

  // Navigate to the application page
  await page.goto('https://demo.playwright.dev/api-mocking');

  // Assert that the mocked Strawberry fruit is displayed
  await expect(page.getByText('Strawberry')).toBeVisible();
});

Modifying API responses

  • Purpose: Intercepts API requests, modifies the response data, and fulfills the request with the modified response.
  • Use case: Testing the application's behavior with different API responses, simulating edge cases, or injecting specific data.
  • Example: In the example below, we intercept the call to the fruit API and modify the data with a new fruit: “Orange.” We then navigate to the URL and verify the presence of this newly added fruit.
test('gets the json from api and adds a new fruit', async ({ page }) => {
  // Get the response and add to it
  await page.route('*/**/api/v1/fruits', async route => {
    const response = await route.fetch();
    const json = await response.json();
    json.push({ name: Orange, id: 100 });
    // Fulfill using the original response while patching the response body
    // with the given JSON object.
    await route.fulfill({ response, json });
  });

  // Go to the page
  await page.goto('https://demo.playwright.dev/api-mocking');

  // Assert that the new fruit is visible
  await expect(page.getByText('Orange', { exact: true })).toBeVisible();
});

Mocking with HAR files

  • Purpose: Using a recorded HAR file to simulate network requests and responses.
  • Use case: Testing an application's behavior with prerecorded network interactions. You can capture HAR files of problematic scenarios and replay them later.
  • Example: The examples below demonstrate how to record, modify, and replay a HAR file:
    • To record a HAR file during a specific test scenario, use Playwright's page.routeFromHAR() or browserContext.routeFromHAR().
    • To modify a HAR file, open the hashed .txt file inside your hars folder and edit the JSON. This file should be kept under source control.
    • To replay a HAR file, turn off or simply remove the update option. This will test against the HAR file instead of hitting the API.

test('gets the json from HAR and checks the new fruit has been added', async ({ page }) => {
 // Replay API requests from HAR.
 // Either use a matching response from the HAR,
 // or abort the request if nothing matches.
 await page.routeFromHAR('./hars/fruit.har', {
   url: '*/**/api/v1/fruits',
   update: false,
 });


// ...

Use CI/CD tools

To effectively leverage Playwright API testing within your software development lifecycle, integrate your test plans with your existing continuous integration (CI) and continuous delivery (CD) pipeline. This integration offers several key benefits in terms of both test execution frequency and test suite maintenance:

  • Early detection: Running tests on every pull request or commit enables you to identify and address issues early in development. However, there might be situations where this isn't practical, such as when tests are computationally expensive.
  • Continuous quality assurance: Regular testing ensures that your API maintains a high level of quality throughout its development lifecycle.
  • Relevance: Regularly review and update your test suites to ensure that they remain relevant to the evolving API and application.
  • Effectiveness: Ensure that your tests cover all critical scenarios and provide meaningful feedback.

Use AI testing tools

AI integrations are transforming the testing process in several key ways. Automated testing solutions use repeated processes to remove rote work while maintaining code quality. 

Traditionally, developers write tests covering all aspects of a feature as it is created. As more features are added, they update previous test cases to streamline the code across the entire application. This continues indefinitely, and testing becomes a bigger overhead as app complexity increases.

AI saves developers time by performing automated code analysis on a given codebase. As code changes are committed and tests are run through CI/CD, faulty tests can be identified and immediately resolved. 

AI-enhanced tools like Qualiti use intelligent analysis to:

  • Triage tests and automatically fix faulty tests
  • Categorize and re-organize tests as code changes are introduced
  • Analyse documents, such as the ability to scan code documentation and retain contextual information

Tools like Qualiti allow users to generate end-to-end Playwright test scripts from simple English prompts, making writing and running tests easier, even for teams with less technical expertise. This speeds up the testing process and ensures more reliable outcomes. For instance, when using Qualiti, you can write a prompt like “Call the notifications endpoint in my API” in the “Type here” box, and the AI will generate the necessary code to script the task.

As an end-to-end tool, Playwright excels in many use cases. Playwright natively supports API testing, and other back-end testing operations. Qualiti builds on this further by allowing testers to create structured prompts to validate responses and endpoints. With natural language prompts, non-technical personnel can be more involved in testing, which can transform team dynamics.

How it works:

1. Natural Language Prompts: Write what you want to test in plain English, and Qualiti generates the appropriate Playwright code. You can break up your test workflow into actions and assertions. Action steps run code operations, while assertion steps ensure test expectations are met.

The Comprehensive Guide to Playwright API Testing

2. AI-Assisted Debugging: Assess test results with the Trace Viewer, which allows you to visually step through each action, ensuring that every interaction functions as expected. Use this to troubleshoot failing tests in production or use it to verify test results in other environments.

The Comprehensive Guide to Playwright API Testing

3. Production Integration: After generating scripts, you can customize them within Playwright, ensuring full control over the tests.

a. Integrating a dedicated production environment into your Qualiti workspace allows you to monitor it.

The Comprehensive Guide to Playwright API Testing

AI tooling allows you to delegate minor testing decisions, freeing time to focus on real issues and alerts. This reduces time to market and eliminates one of the engineering world's most notorious challenges: flaky tests.

{{banner-small-4="/banners"}}

Last thoughts

Understanding an API's structure, security mechanisms, and network considerations can help you create comprehensive and efficient test suites with high coverage. Leveraging Playwright's capabilities, including mocking, HAR file manipulation, and integration with CI/CD tools, can streamline your API testing and delivery process. 

Starting small is crucial for automation because it allows teams to gradually build and validate their processes. By first establishing and verifying a well-defined testing plan, including preconditions and configurations, they can ensure a solid foundation before scaling up. From there, test coverage can be gradually expanded as needed. Continuously refining and improving your test suite in response to feedback and evolving requirements ensures that your automation remains effective and relevant over time. 

By leveraging Qualiti’s AI-powered testing tools, you can automate the creation and maintenance of your test suite, freeing up your team to focus on higher-value tasks. Quickly set up your testing environment and generate detailed tests by adding context for the AI, all while maintaining control over the testing process.