Playwright: GraphQL Requests in A Utility for Efficient Testing

matan3sh

Matan Shaviro

Posted on November 23, 2024

Playwright: GraphQL Requests in A Utility for Efficient Testing

When working with end-to-end testing frameworks like Playwright, mocking GraphQL requests can significantly improve test reliability and speed. Inspired by Jay Freestone's excellent blog post, Stubbing GraphQL Requests in Playwright, I decided to build a reusable utility function that allows for flexible GraphQL request interception and response stubbing.

In this post, I’ll walk you through my implementation of the interceptGQL utility and demonstrate how it can be used with Playwright to mock server responses for GraphQL queries and mutations.

The interceptGQL Utility: How It Works

The interceptGQL utility registers a route handler for all GraphQL requests to your backend, intercepting specific operations based on their operationName. You can define how each operation should respond and validate the variables passed in the request.

Here’s the implementation:

import { test as baseTest, Page, Route } from '@playwright/test';
import { namedOperations } from '../../src/graphql/autogenerate/operations';

type CalledWith = Record<string, unknown>;

type Operations = keyof (typeof namedOperations)['Query'] | keyof (typeof namedOperations)['Mutation'];

type InterceptConfig = {
  operationName: Operations | string;
  res: Record<string, unknown>;
};

type InterceptedPayloads = {
  [operationName: string]: CalledWith[];
};

export async function interceptGQL(
  page: Page,
  interceptConfigs: InterceptConfig[]
): Promise<{ reqs: InterceptedPayloads }> {
  const reqs: InterceptedPayloads = {};

  interceptConfigs.forEach(config => {
    reqs[config.operationName] = [];
  });

  await page.route('**/graphql', (route: Route) => {
    const req = route.request().postDataJSON();
    const operationConfig = interceptConfigs.find(config => config.operationName === req.operationName);

    if (!operationConfig) {
      return route.continue();
    }

    reqs[req.operationName].push(req.variables);

    return route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ data: operationConfig.res }),
    });
  });

  return { reqs };
}

export const test = baseTest.extend<{ interceptGQL: typeof interceptGQL }>({
  interceptGQL: async ({ browser }, use) => {
    await use(interceptGQL);
  },
});

Enter fullscreen mode Exit fullscreen mode

Example: Testing a Task Management Dashboard

To demonstrate the utility in action, let’s use it to test a Task Management Dashboard. We'll intercept a GraphQL query (GetTasks) and mock its response.

import { expect } from '@playwright/test';
import { namedOperations } from '../../../src/graphql/autogenerate/operations';
import { test } from '../../fixtures';
import { GetTasksMock } from './mocks/GetTasks.mock';

test.describe('Task Management Dashboard', () => {
  test.beforeEach(async ({ page, interceptGQL }) => {
    await page.goto('/tasks');

    await interceptGQL(page, [
      {
        operationName: namedOperations.Query['GetTasks'],
        res: GetTasksMock,
      },
    ]);
  });

  test('Should render a list of tasks', async ({ page }) => {
    const taskDashboardTitle = page.getByTestId('task-dashboard-title');
    await expect(taskDashboardTitle).toHaveText('Task Dashboard');

    const firstTaskTitle = page.getByTestId('0-task-title');
    await expect(firstTaskTitle).toHaveText('Implement authentication flow');

    const firstTaskStatus = page.getByTestId('0-task-status');
    await expect(firstTaskStatus).toHaveText('In Progress');
  });

  test('Should navigate to task details page when a task is clicked', async ({ page }) => {
    await page.getByTestId('0-task-title').click();

    await expect(page.getByTestId('task-details-header')).toHaveText('Task Details');
    await expect(page.getByTestId('task-details-title')).toHaveText('Implement authentication flow');
  });
});

Enter fullscreen mode Exit fullscreen mode

What's Happening Here?

  1. Intercepting Requests: The interceptGQL utility intercepts the GetTasks query and returns the mock data defined in GetTasksMock.
  2. Mocking Responses: The mocked response is served instead of hitting the actual backend.
  3. Validating Variables: The utility also stores the GraphQL variables sent with the request, which can be useful for testing API calls in isolation.

Why Use This Approach?

  1. Improved Speed: By avoiding actual network requests, tests run faster and more reliably.
  2. Simplified Test Data: You control the responses, making it easier to test edge cases and various application states.
  3. Validation of API Calls: By capturing the variables sent with the request, you can ensure the frontend calls the backend with the correct parameters.

This implementation and approach were inspired by Jay Freestone's excellent blog post, Stubbing GraphQL Requests in Playwright. His post provided a solid foundation for building the interceptGQL utility.

By incorporating this utility into your Playwright test suite, you can mock GraphQL queries and mutations with ease, improving test speed and reliability while simplifying complex scenarios.

💖 💪 🙅 🚩
matan3sh
Matan Shaviro

Posted on November 23, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related