Thursday, October 1, 2026

Playwright API Mock Project Structure

Playwright API Mock Project Structure

In modern web development, testing applications with Playwright requires a robust approach to handling external dependencies. Creating separate mock data, mock implementation, and tests is crucial for building reliable, maintainable test suites that can run consistently regardless of external API availability. This separation enhances test readability, makes maintenance easier, and ensures test reliability by isolating different components, allowing you to modify one aspect without affecting others.

A scalable Playwright mock framework separates the project into three main layers: mock data, mock implementation, and tests.


A scalable Playwright mock framework separates the project into three main layers: mock data, mock implementation, and tests.

Recommended Project Structure

e2e/
│
├── tests/
│   ├── users.spec.js
│   ├── dashboard.spec.js
│   └── login.spec.js
│
├── mocks/
│   ├── data/
│   │   ├── user.data.js
│   │   ├── dashboard.data.js
│   │   └── login.data.js
│   │
│   ├── user.mock.js
│   ├── dashboard.mock.js
│   ├── login.mock.js
│   └── index.js
│
├── utils/
│   └── apiMock.js
│
├── fixtures/
│   └── test.fixture.js
│
└── playwright.config.js

1. Generic API Mock Utility

// utils/apiMock.js

export class ApiMock {
  constructor(page) {
    this.page = page;
  }

  async get(url, response, status = 200) {
    await this.page.route(url, async route => {
      await route.fulfill({
        status,
        contentType: 'application/json',
        body: JSON.stringify(response)
      });
    });
  }

  async post(url, response, status = 200) {
    await this.page.route(url, async route => {
      if (route.request().method() !== 'POST') {
        await route.continue();
        return;
      }

      await route.fulfill({
        status,
        contentType: 'application/json',
        body: JSON.stringify(response)
      });
    });
  }

  async put(url, response, status = 200) {
    await this.page.route(url, async route => {
      if (route.request().method() !== 'PUT') {
        await route.continue();
        return;
      }

      await route.fulfill({
        status,
        contentType: 'application/json',
        body: JSON.stringify(response)
      });
    });
  }

  async delete(url, response = {}, status = 204) {
    await this.page.route(url, async route => {
      if (route.request().method() !== 'DELETE') {
        await route.continue();
        return;
      }

      await route.fulfill({
        status,
        contentType: 'application/json',
        body: JSON.stringify(response)
      });
    });
  }
}

2. Mock Data

// mocks/data/user.data.js

export const userData = {
  success: {
    id: 1,
    name: 'Sumit',
    email: 'sumit@example.com',
    role: 'admin'
  },

  multipleUsers: {
    users: [
      {
        id: 1,
        name: 'Sumit',
        email: 'sumit@example.com'
      },
      {
        id: 2,
        name: 'John',
        email: 'john@example.com'
      }
    ]
  },

  error: {
    message: 'Internal Server Error'
  },

  unauthorized: {
    message: 'Unauthorized'
  }
};

3. User API Mock

// mocks/user.mock.js

import { ApiMock } from '../utils/apiMock.js';
import { userData } from './data/user.data.js';

export class UserMock {

  constructor(page) {
    this.api = new ApiMock(page);
  }

  async getSuccess() {
    await this.api.get(
      '**/api/users/1',
      userData.success
    );
  }

  async getMultipleUsers() {
    await this.api.get(
      '**/api/users',
      userData.multipleUsers
    );
  }

  async getServerError() {
    await this.api.get(
      '**/api/users/1',
      userData.error,
      500
    );
  }

  async getUnauthorized() {
    await this.api.get(
      '**/api/users/1',
      userData.unauthorized,
      401
    );
  }
}

4. Central Mock Object

// mocks/index.js

import { UserMock } from './user.mock.js';
import { DashboardMock } from './dashboard.mock.js';

export class Mock {

  constructor(page) {
    this.user = new UserMock(page);
    this.dashboard = new DashboardMock(page);
  }
}

5. Using the Mock in Tests

// tests/users.spec.js

import { test, expect } from '@playwright/test';
import { Mock } from '../mocks/index.js';

test('display user details', async ({ page }) => {

  const mock = new Mock(page);

  await mock.user.getSuccess();

  await page.goto('/users/1');

  await expect(
    page.getByText('Sumit')
  ).toBeVisible();

  await expect(
    page.getByText('sumit@example.com')
  ).toBeVisible();
});

6. API Error Scenario

test('display API error', async ({ page }) => {

  const mock = new Mock(page);

  await mock.user.getServerError();

  await page.goto('/users/1');

  await expect(
    page.getByText('Internal Server Error')
  ).toBeVisible();
});

7. Recommended Architecture

                 Playwright Test
                        │
                        ▼
                   Mock Object
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
      UserMock     DashboardMock   LoginMock
          │             │             │
          ▼             ▼             ▼
      User Data    Dashboard Data   Login Data
          │             │             │
          └─────────────┼─────────────┘
                        ▼
                     ApiMock
                        │
                        ▼
                   page.route()

8. Final Test Usage

test('user test', async ({ page }) => {

  const mock = new Mock(page);

  await mock.user.getSuccess();

  await page.goto('/users/1');

  await expect(
    page.getByText('Sumit')
  ).toBeVisible();

});

Benefits

  • Tests remain clean and focused on business behavior.
  • Mock response data is maintained separately.
  • API mocking logic is reusable across tests.
  • Success and error scenarios can be added easily.
  • New APIs can be added as separate mock classes.
  • The structure works well for an Nx Playwright monorepo.

No comments:

Post a Comment