API Mocking for your Playwright tests

Debbie O'Brien - Jul 4 '23 - - Dev Community

When working with third party API's it is better to mock the API call rather than hit the API especially when they are API's that you do not control. You might also want to mock an API when in development mode and the API hasn't been written yet. Mocking the API allows you to finish developing your component or app and write the tests and then when the API is ready you can just swap out the mock for the real API call.

With Playwright you don't need any additional libraries to mock an API call. You can use the page.route method to intercept the API call and return a mock response. This means that instead of hitting the real API the browser will return the mocked response.

Mocking the API call

In the example below we are intercepting an API call to */**/api/v1/fruits and returning a mock response of [{ name: 'Strawberry', id: 21 }] by using the fulfill method, which fulfills a route's request with a given response.

await page.route('*/**/api/v1/fruits', async (route) => {
  const json = [{ name: 'Strawberry', id: 21 }];
  await route.fulfill({ json });
});
Enter fullscreen mode Exit fullscreen mode

Let's take a look at this with a real example. We have an app that fetches and renders a list of fruit. We want to test that the page renders a fruit but we don't want to hit the API each time we run the test.

We can intercept the browser API call to */**/api/v1/fruits and pass in a mock response that we want to be fulfilled by our route.

We then go to the page and assert that the page has a text of 'Strawberry', the mocked data that we created.

test('mocks a fruit and does not call api', async ({ page }) => {
  // Mock the api call before navigating
  await page.route('*/**/api/v1/fruits', async (route) => {
    const json = [{ name: 'Strawberry', id: 21 }];
    await route.fulfill({ json });
  });

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

  // Assert that the Strawberry fruit is visible
  await expect(page.getByText('Strawberry')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

list of fruits with only strawberry in list

When running our test with UI Mode, or with the view traces option from the VS Code extension, we can inspect the network tab and see that our API call of 'fruits' has the 'fulfilled' tag next to it. This means that the API call has been intercepted and the mock response has been returned.

api mocking trace

Modifying the API call

Sometimes you might need to modify the API call to add more data to the response until it has been implemented in the API.

Instead of mocking the API we can intercept the route just like in the example above but instead we will use the route.fetch() method. This performs the request and fetches the result, so that the response can be modified and then fulfilled using the route.fulfill() method.

In the fulfill method we pass in the response argument, which is the API response to fulfill the route's request with, and the json argument, which will contain the new fruit which we will call 'Playwright'.

await page.route('*/**/api/v1/fruits', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.push({ name: "Playwright", id: 100 });
  await route.fulfill({ response, json });
});
Enter fullscreen mode Exit fullscreen mode

In our complete example it will look something like this where we fetch the API response and add Playwright as the new fruit. We then go to the page and assert that the Playwright text exits.

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: "Playwright", 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('Playwright', { exact: true })).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

When we run our test we can now see that our new fruit which we called 'Playwright' is visible on the page.

list of fruit showing Playwright in list

If we run our test with with UI Mode, or with the view traces option from the VS Code extension, we can see that we are using route.fetch. If we open the network tab on the page.goto step we can see our 'fruits' call has the 'api' tag next to it, followed by the 'fulfilled' tag. This means that the API has been called and the response has been modified.

trace viewer api call

Mocking with HAR files

A HAR file is an HTTP Archive file that contains a record of all the network requests that are made when a page is loaded. It contains information about the request and response headers, cookies, content, timings, and more. You can use HAR files to mock network requests in your tests.

To record a HAR file we use the routeFromHAR method. This method takes in the path to the HAR file and an optional object of options.

The options object can contain the URL so that only requests with the URL matching the specified glob pattern will be served from the HAR File. If not specified, all requests will be served from the HAR file.

The update option updates the given HAR file with the actual network information instead of serving from the file. In order to record the HAR file, you need to set update to true.

await page.routeFromHAR('./hars/fruits.har', {
  url: '*/**/api/v1/fruits',
  update: true,
});
Enter fullscreen mode Exit fullscreen mode

Let's take a look at how we would write our test using a HAR file. We start by recording the HAR file by setting the url option to our API and the update option to true and then go to our apps page to record the API call.

test('records or updates the HAR file', async ({ page }) => {
  // Get the response from the HAR file
  await page.routeFromHAR('./hars/fruits.har', {
    url: '*/**/api/v1/fruits',
    update: true,
  });

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

// Assert that the fruit is visible
  await expect(page.getByText('Strawberry')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

When you run the test you will see that the HAR file has been recorded in the hars folder. You can open the HAR file and see the request and response information. Under the content section of the fruits.har file you will see the name of a '.txt' file with a hashed name. This file contains the JSON response from your API call and is located inside the hars folder.

"content": {
  "size": -1,
  "mimeType": "text/plain; charset=utf-8",
  "_file": "071271e20ae0915b74df7103cbde3151afa4c4df.txt"
},
Enter fullscreen mode Exit fullscreen mode

When you open the .txt file you will see the full result of your API response. You can now use this HAR file in your test to mock the API call meaning you are testing against the real API data without having to make the API call each time.

To run our test against the HAR file we just have to set 'update' to false or remove it completely.

test('gets the json from HAR and checks a fruit is visible', 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/fruits.har', {
    url: '*/**/api/v1/fruits',
    update: false,
  });

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

  // Assert that the Playwright fruit is visible
  await expect(page.getByText('Strawberry')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

Modifying the HAR file

You can also modify the response from the HAR file and then run your tests against the modified response. This is useful if you want to add some new data to your response before it has been implemented on the API. This file should be committed to your source control. Anytime you run this test with update: true it will update your HAR file with the request from the API and override any manual changes.

[
  {
    "name": "Playwright",
    "id": 100
  },
  // ... other fruits
]
Enter fullscreen mode Exit fullscreen mode

We can now run our test against the modified HAR file and assert that the Playwright fruit is visible on the page.

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/fruits.har', {
    url: '*/**/api/v1/fruits',
    update: false,
  });

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

  // Assert that the Playwright fruit is visible
  await expect(page.getByText('Playwright', { exact: true })).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

When we inspect the trace of our test we can see our new fruit called Playwright added to the top of the list.

list of fruit showing Playwright

When we click on the next step of our test and open the network tab we can see that the 'fruits' file has been fulfilled meaning we are using the HAR file to mock the API call.

trace viewer route fulfilled

We can expand the network call and scroll down to inspect the response body. Here we can see our Playwright fruit has been added.

trace view of response

Conclusion

With Playwright you can intercept Browser HTTP requests and run your tests against the mock data with the route and fulfill methods. You can also intercept the API call and modify the response by passing in the response and your modified data to the route.fulfill method. You can use the routeFromHAR method to record the API call and response and then use the HAR file to run your tests against instead of hitting the API each time. You can also modify the HAR file and run your tests against the modified data.

Useful Links

. . . . . . . . . . . . . . . . . . . .