All Articles

Http calls mocking in Playwright

In one of my previous articles about decoupling E2E tests I was reasoning about why we should switch from full-blown E2E tests to much more isolated UI tests. And in this article, I will show you how to do it in Playwright.

To isolate our UI tests, we need to mock the API. That means we need to “catch” the outgoing request and return some static data based on it. Luckily, Playwright has a built-in method for it - route.fulfill([options]). Imagine we have an application, that calls the/items API endpoint for fetching all items available. To isolate it from the actual API we could create the following function:

export const itemsRoute = (page) => page.route('**/items', route => {
  route.fulfill({
    body: JSON.stringify([{ id: 1, name: "first item"}, { id: 2, name: "second item"}])
  });
});

And import it from within our test block or beforeEach method:

beforeEach(async () => {
    await itemsRoute(page)
})

And that’s it, once the /items endpoint is called it will never reach the server, but it gets mocked within the browser context and returned with specified body and status code 200. The 200 status code is default and you can change it by providing status property with desired status code in fulfill options. The following example will return 301 status code for /items` call:

export const itemsRoute = (page: Page): Promise<void> => page.route('**/items', route => {
  route.fulfill({
    status: 301,
    body: JSON.stringify([{ id: 1, name: "first item"}, { id: 2, name: "second item"}])
  });
});

For a list of all options navigate to offical docs.

Let’s have a look at one more scenario - it’s very common that any kind of listing in today’s web application support ordering and other functionality that changes the returned values based on the input criteria. But for simplicity, we only consider ordering in our example. The ordering works on the API level by appending parameters to the API request, eg: /items?order=asc and /items?order=desc. We could register a new route for each call, but that would be too verbose and require us to clutter our test code with unnecessary imports. A better way could be achieved by using a map with the parameters and their mocked responses.

const itemsParamMap = new Map([
    ["order=asc", [{ id: 1, name: "first item"}, { id: 2, name: "second item"}] ],
    ["order=desc, [{ id: 2, name: "second item"}, { id: 1, name: "first item"}] ],
])

export const itemsRoute = (page: Page): Promise<void> => page.route(`**/items**`, route => {
    const requestUrl = route.request().url()
    const params = requestUrl.split("?")[1]
    const responseBody = itemsParamMap.get(params)
    if (responseBody) {
        route.fulfill({
            headers: {
                "Access-Control-Allow-Origin": "*",
            },
            body: JSON.stringify(responseBody),
        })
    }
})

This way we implemented clean and concise mocking based on the parameters of the request. And we only need to import itemsRoute once, and all matching API calls will get mocked automatically and our UI tests remain independent from the real API.


Read more about Playwright:

Published Oct 13, 2021

Thoughts on testing and test automation