DEV Community

Michael Gwynne
Michael Gwynne

Posted on

Server Side Mocking for Playwright in NextJS using Mock Service Worker

After cycling through several technologies, I settled on Playwright for the e2e testing of my NextJS app.

If the Playwright tests fail, the CI for that branch will fail and disable Github's merging button. Great. 👍

However, if any of the APIs I rely on are down/broken/misbehaving, this can cause my Playwright tests to fail. And in the case of screenshot testing, updated data in these APIs can again cause the tests to fail.

"Aha!" I thought, "Mock Service Worker can help with this". However, all documentation and articles I found were aimed towards client-side request mocking rather than server side.
Luckily I stumbled across a very useful article by Dominik Ferber which, after some trial and error, allowed me to mock my server-side requests.
While the article really helped me, I feel it was overly complex for my use-case, slightly out-of-date and missed some key points I found myself which I think could be beneficial to others.

Set up a demo app

Create a new NextJS install.
Fetch some data from an API in your page:

// src/pages/index.tsx
export async function getServerSideProps() {
  const res = await fetch('https://pokeapi.co/api/v2/pokemon/bulbasaur');
  const bulbasaur = await res.json();

  return {
    props: {
      bulbasaur
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

And update your page to use the result:

// src/pages/index.tsx
export default function Home({bulbasaur}: {bulbasaur: {name: string}}) {
  ...
  <h1>{bulbasaur.name}</h1>
Enter fullscreen mode Exit fullscreen mode

Setting up Playwright

Firstly, let's set up Playwright:
Install Playwright and add "test": "playwright test" to package.json.
Playwright tests a running version of your app. So allow Playwright to start a server that runs your app:

// playwright.config.ts
module.exports = {
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    timeout: 120 * 1000,
  }
}
Enter fullscreen mode Exit fullscreen mode

Then add a test:

// tests/index.spec.ts
import { expect, test} from "@playwright/test";

test("Bulbasaur", async ({page}) => {
  await page.goto(`http://localhost:3000/`);
  const name = await page.innerText('h1');
  expect(name).toBe('bulbasaur');
});
Enter fullscreen mode Exit fullscreen mode

Run npm run build && npm run test and you should see your tests pass.
Great.

However, if this API was down or modified in some way, your test would fail. If you mock the result, you can be confidant that any broken tests are a result of your code.

Setting up Mock Service Worker

Install MSW via NPM.
Create a set of Playwright fixtures for the server side mocking:

// tests/fixture.ts
import {test as base} from "@playwright/test";
import {createServer, Server} from "http";
import {rest} from "msw";
import type {SetupServerApi} from "msw/node";
import {AddressInfo} from "net";
import next from "next";
import path from "path";
import {parse} from "url";

const test = base.extend<{ port: string; requestInterceptor: SetupServerApi; rest: typeof rest; }>({
  port: [
    async ({}, use) => {
      const app = next({dev: false, dir: path.resolve(__dirname, "..")});
      await app.prepare();

      const handle = app.getRequestHandler();

      const server: Server = await new Promise(resolve => {
        const server = createServer((req, res) => {
          const parsedUrl = parse(req.url, true);
          handle(req, res, parsedUrl);
        });

        server.listen((error) => {
          if (error) throw error;
          resolve(server);
        });
      });
      const port = String((server.address() as AddressInfo).port);
      await use(port);
    },
    {
      scope: "worker",
      auto: true
    }
  ],
  requestInterceptor: [
    async({}, use) => {
      await use((() => {
        const {setupServer} = require("msw/node");
        const requestInterceptor = setupServer();

        requestInterceptor.listen({
          onUnhandledRequest: "bypass"
        });

        return requestInterceptor
      })());
    },
    {
      scope: "worker"
    }
  ],
  rest
});

export default test;
Enter fullscreen mode Exit fullscreen mode

Update your test file:

// tests/index.spec.ts
import {expect} from "@playwright/test";
import test from "./fixture";

test("Bulbasaur", async ({page, port, rest, requestInterceptor}) => {
  requestInterceptor.use(
    rest.get('https://pokeapi.co/api/v2/pokemon/bulbasaur', (req, res, ctx) =>
      res(
        ctx.json({name: 'squirtle'})
      )
    )
  );
  await page.goto(`http://localhost:${port}/`);
  const name = await page.innerText('h1');
  expect(name).toBe('squirtle');
});
Enter fullscreen mode Exit fullscreen mode

You can see that we have mocked the response of the name of "bulbasaur" to be "squirtle" just to prove that we are getting the mocked result.
Since the "port" fixture is creating a new server for our app to run on, we can also delete our previously created playwright.config.ts.
Run npm run build && npm run test again and you should see your tests pass.

So what is happening here?

Before we are running our tests, we are building our production-like app. The "port" fixture is then creating it's own server to run our app on. We can then use the requestInterceptor to intercept calls to certain URLs that the aforementioned server is making and mock their responses.

Bonus

In my case, I had a customised server setup which ran during npm run start. I extracted all the important bits in to a separate file so that I could call them both as part of npm run start and in the "port" fixture.

// tests/fixture.ts
const server: Server = await new Promise(resolve => {
  const server = setUpMyServer(handle);

  server.listen((error) => {
    if (error) throw error;
    resolve(server);
  });
});
Enter fullscreen mode Exit fullscreen mode

What else?

Top comments (0)