How do I mock server-side API calls in a Nextjs app?
Asked Answered
T

1

12

I'm trying to figure out how to mock calls to the auth0 authentication backend when testing a next js app with React Testing Library. I'm using auth0/nextjs-auth0 to handle authentication. My intention is to use MSW to provide mocks for all API calls.

I followed this example in the nextjs docs next.js/examples/with-msw to set up mocks for both client and server API calls. All API calls generated by the auth0/nextjs-auth0 package ( /api/auth/login , /api/auth/callback , /api/auth/logout and /api/auth/me) received mock responses.

A mock response for /api/auth/me is shown below

import { rest } from 'msw';

export const handlers = [
  // /api/auth/me
  rest.get(/.*\/api\/auth\/me$/, (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        user: { name: 'test', email: '[email protected]' },
      }),
    );
  }),
];

The example setup works fine when I run the app in my browser. But when I run my test the mocks are not getting picked up.

An example test block looks like this

import React from 'react';
import {render , screen } from '@testing-library/react';

import Home from 'pages/index';
import App from 'pages/_app';

describe('Home', () => {
  it('should render the loading screen', async () => {
    render(<App Component={Home} />);
    const loader = screen.getByTestId('loading-screen');
    expect(loader).toBeInTheDocument();
  });
});

I render the page inside the App component like this <App Component={Home} /> so that I will have access to the various contexts wrapping the pages.

I have spent about 2 days on this trying out various configurations and I still don't know what I might be doing wrong. Any and every help is appreciated.

Teethe answered 4/2, 2022 at 1:11 Comment(3)
Where do you setup the MSW server instance to intercept the requests during the tests?Iey
I setup the server instance inside the beforeAll callback setupTests.js.Teethe
@noviceGuru I eventually moved on in the project. But the answer by sven sense.Teethe
A
6

This is probably resolved already for the author, but since I ran into the same issue and could not find useful documentation, this is how I solved it for end to end tests:

Overriding/configuring the API host.


The plan is to have the test runner start next.js as custom server and then having it respond to both the next.js, as API routes.


A requirements for this to work is to be able to specify the backend (host) the API is calling (via environment variables). Howerver, access to environment variables in Next.js is limited, I made this work using the publicRuntimeConfig setting in next.config.mjs. Within that file you can use runtime environment variables which then bind to the publicRuntimeConfig section of the configuration object.

/** @type {import('next').NextConfig} */
const nextConfig = {
  (...)
  publicRuntimeConfig: {
    API_BASE_URL: process.env.API_BASE_URL,
    API_BASE_PATH: process.env.API_BASE_PATH,
  },
  (...)
};

export default nextConfig;

Everywhere I reference the API, I use the publicRuntimeConfig to obtain these values, which gives me control over what exactly the (backend) is calling.


Allowing to control the hostname of the API at runtime allows me to change it to the local machines host and then intercept, and respond to the call with a fixture.


Configuring Playwright as the test runner.

My e2e test stack is based on Playwright, which has a playwright.config.ts file:

import type { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
  globalSetup: './playwright.setup.js',
  testMatch: /.*\.e2e\.ts/,
};

export default config;

This calls another file playwright.setup.js which configures the actual tests and backend API mocks:

import {createServer} from 'http';
import {parse} from 'url';
import next from 'next';
import EndpointFixture from "./fixtures/endpoint.json";

// Config
const dev = process.env.NODE_ENV !== 'production';
const baseUrl = process?.env?.API_BASE_URL || 'localhost:3000';

// Context
const hostname = String(baseUrl.split(/:(?=\d)/)[0]).replace(/.+:\/\//, '');
const port = baseUrl.split(/:(?=\d)/)[1];
const app = next({dev, hostname, port});
const handle = app.getRequestHandler();

// Setup
export default async function playwrightSetup() {
    const server = await createServer(async (request, response) => {
        // Mock for a specific endpoint, responds with a fixture.
        if(request.url.includes(`path/to/api/endpoint/${EndpointFixture[0].slug}`)) {
            response.write(JSON.stringify(EndpointFixture[0]));
            response.end();
            return;
        }

        // Fallback for pai, notifies about missing mock.
        else if(request.url.includes('path/to/api/')) {
            console.log('(Backend) mock not implementeded', request.url);
            return;
        }

        // Regular Next.js behaviour.
        const parsedUrl = parse(request.url, true);
        await handle(request, response, parsedUrl);
    });

    // Start listening on the configured port.
    server.listen(port, (error) => {
        console.error(error);
    });

    // Inject the hostname and port into the applications publicRuntimeConfig.
    process.env.API_BASE_URL = `http://${hostname}:${port}`;
    await app.prepare();
}

Using this kind of setup, the test runner should start a server which responds to both the routes defined by/in Next.js as well as the routes intentionally mocked (for the backend) allowing you to specify a fixture to respond with.


Final notes

Using the publicRuntimeConfig in combination with a custom Next.js servers allows you to have a relatively large amount of control about the calls that are being made on de backend, however, it does not necessarily intercept calls from the frontend, the existing frontend mocks might stil be necessary.

Aleras answered 4/1, 2023 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.