DEV Community

Weam Adel
Weam Adel

Posted on

Configure Playwright with Next.js & Mock APIs for Testing

Motivation

Although playwright's docs is straight forward when it comes to configuring with next.js, things like API mocks lack proper docs from both sides. Also the blogs that I found online were so outdated and buggy.
This motivated me to write about my outcomes after long hours of searching and testing my finding until I finally managed to get things to work.

Disclaimer

As the current mocking feature is experimental in Next.js, I will share with you the versions for both next.js and playwright, so that if things did not work with you, you may need to check what changed in the newer versions (instead of cursing me 😔)

Package Version
next 14.0.4
@playwright/test 1.46.1

Install & Configure Playwright

The first step of course is to install and config playwright with your Next.js app.
You can follow this install playwright docs to install playwright, but all what you need basically is to run the following command then answer some questions in the terminal and playwright will configure everything for you.

npm init playwright@latest
Enter fullscreen mode Exit fullscreen mode

Here I named my e2e testing directory to e2e instead of the default naming.

installing result

Commands

You can test if everything is working or not by running the next command. This will run the example tests added by playwright.

npx playwright test
Enter fullscreen mode Exit fullscreen mode

This command runs the tests in an non-interactive mode, so like jest you will get the results in the terminal as follows.

running tests

Test Interactive UI

npx playwright test --ui
Enter fullscreen mode Exit fullscreen mode

This is in case you want to see the test running in the browser. This can be so useful in debugging, as you get to inspect the DOM, see rendering errors, track events like click events and more. Here is how it looks like when testing the example tests:

running tests interactively

Helpers

As we will run these test commands so frequently, you can add them to your package.json and use the aliases directly like so npm run test:e2e-ui.
package.json

{ 
  "test:e2e": "npx playwright test",
  "test:e2e-report": "npx playwright show-report",
  "test:e2e-ui": "npx playwright test --ui"
}
Enter fullscreen mode Exit fullscreen mode

Using Playwright

As writing tests is not the topic for this blog, and I just wanted to show you how to configure playwright with next and mock the APIs with next server, you can refer to playwright docs to learn how to write tests.

APIs Mock

Why?

You do not want to hit the real server each time you run a test for lots of reasons:

  • It may cost you money if you use an external service like Amazon.
  • It may manipulate the real data on the server.
  • It puts load on your server.
  • It make your tests slow, as you need to reach out to the server and load the data.
  • You need to test your own code in separation of other external factors, so your tests should not be affected if the server is down.

Note

If you are using react without next.js and server components, or if you are fetching some of your data on the client, then you need to follow playwright docs for mocking APIs, as your fetch happens in the browser.
In next.js case, we need different configs as the fetching in next.js happens on the server (mainly), so we need to intercept it in a different way.

Our Page

Here we have a dummy post details page that we want to test. This page is a server component and we are fetching dummy data using json placeholder to fetch fake post details, so basically we need to mock https://jsonplaceholder.typicode.com/posts/1 endpoint in our tests.

app/posts/[id]/page.tsx
page tsx code

Mock Configs

Following this anonymous docs link for configuring playwright with next that was super easy to find 😡, you need to install this package that will provide you with test utils specified for intercepting next.js functionalities during testing with playwright.

npm install -D @playwright/test
Enter fullscreen mode Exit fullscreen mode

And in your next.config.ts file:

module.exports = {
  experimental: {
    testProxy: true,
  },
}
Enter fullscreen mode Exit fullscreen mode

Note: If you receive a warning while running the dev server that this testProxy is not a valid config, do not worry, you may be using an older next.js version, so instead, run the dev server for testing using the flag -–experimental-test-proxy. I created an alias for it for convenience:

package.json

{
  "dev:test": "next dev --experimental-test-proxy"
}
Enter fullscreen mode Exit fullscreen mode

If you are using the flag and do not want to run npm run dev:test manually before running your tests, go to playwright.config.ts and uncomment the webServer option (or add it if not found) and just change the command to run your alias.
If you are using the experimental textProxy prop in next.config.ts then you do not need to do this command renaming nor the flag step.

playwright.config.ts

export default defineConfig({
  testDir: "./e2e",
 /* Run your local dev server before starting the tests */
  webServer: {
    command: "npm run dev:test",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});
Enter fullscreen mode Exit fullscreen mode

Note: You will get errors if you try to visit your app served with the test proxy flag, so this command is to run the server for your tests only.

Testing The Component

Let’s jump to the main purpose, mocking the API. Here is our directory hierarchy:

  • Mocks: where we have our mock-apis functionality that we will see next.
  • Data: our mock (fake) data that we want to provide instead of the actual data that we would get when hitting the server.
  • post.spec.ts: Our test file to test the post details page.

folder hierarchy

Mocked Data

First of all let’s add our fake post data into posts.json

{
  "title": "Creative Post Title",
  "userId": 1,
  "id": 1,
  "body": "This post is about something creative."
}
Enter fullscreen mode Exit fullscreen mode

Post Test File

Second Let’s have a look at what the test file should look like before adding the mocking configs, then we will see what the mockApiEndPoints and its reset function do.
Here we use the test and expect methods provided by the package we installed, instead of the original test and expect that come with playwright by default. Those methods will do some work for us under the hood to intercept some next.js functionality so that we can provide our own functionality.

Then before we visit the page we need to mock the APIs. We can either use the default data from the json file, or override all or part of it, as in the second test example.
After that we reset any mocks to restore the original functionality.

post.spec.ts

import { test, expect } from 'next/experimental/testmode/playwright';
import { mockAPIEndpoints, resetAPIEndpointsMock } from './mocks/api/mock-apis';

test.describe('post details', () => {
    test('should show the post title', async ({ next, page }) => {
        // Mock the API response with default data
        mockAPIEndpoints(next);
        await page.goto('http://localhost:3000/posts/1');

        await expect(page.getByTestId('post-title')).toHaveText(/Creative Post Title/i);
    });

    test('should show custom post title', async ({ next, page }) => {
        // Mock the API response with custom data, notice we are only overriding the title
        mockAPIEndpoints(next, {
            postDetails: {
                title: "It's Easy",
            },
        });
        await page.goto('http://localhost:3000/posts/1');

        await expect(page.getByTestId('post-title')).toHaveText(/It's Easy/i);
    });
});

// Reset the API mocks after each test
test.afterEach(async ({ next }) => {
    resetAPIEndpointsMock(next);
});
Enter fullscreen mode Exit fullscreen mode

Mock The APIs

The main function we want to implement here is mockAPIEndPoints, but we need other util functions as well to make it work.

apiMockOverrides Type
First things first, the ApiMockOverrides type is to define the shape of the data that I want to provide in case I want to override the default data coming from the json file.
Here we have only the postDetails endpoint, so we add its type as we will need to test different scenarios.

getMockedResponse Function
It’s a util function that fakes a response object and configures any headers or configs necessary to the response.

mockApiEndPoints Function
It’s where we actually mock the APIs. It takes a next instance passed from the test as we saw before in the test file. It also takes an optional overrides object in case we need to override one or more pieces of the default data.

The next instance has onFetch method to intercept any fetching happening, and then we check if it’s trying to call a specific API endpoint, we catch this and return our own response with our data, otherwise, abort, as we do not want to hit the actual server with the actual endpoint accidently.

resetAPIEndpointsMock Function
As a best practice, we clean up and reset any mocked functionality.

mock-apis.ts

import type { BrowserContext, NextFixture } from 'next/experimental/testmode/playwright';
import postData from '../data/posts.json';
import { Post } from '@/types/Post';

interface ApiMockOverrides {
    postDetails?: Partial<Post>;
    // otherEndpointOverrideData?: any;
}

/**
 * Get a response with mocked data
 */
export function getMockedResponse(data: any) {
    return new Response(JSON.stringify(data), {
        headers: {
            'Content-Type': 'application/json',
        },
    });
}

/**
 * Mock API endpoints
 */
export async function mockAPIEndpoints(next: NextFixture, overrides: ApiMockOverrides = {}) {
    next.onFetch((request: Request) => {
        // Post Details
        if (request.url.endsWith('/posts/1')) {
            return getMockedResponse({ ...postData, ...overrides.postDetails });
        }

        // Another endpoint
        // if (request.url.endsWith(`/api/endpoint`)) {
        //   return getMockedResponse({ ...data, ...overrides.endpoint });
        // }

        console.log(`No API mocks found for url ${request.url}`);

        return 'abort';
    });
}

/**
 * Reset the API endpoints mock
 */
export async function resetAPIEndpointsMock(next: NextFixture) {
    next.onFetch((request: Request) => "continue");
}
Enter fullscreen mode Exit fullscreen mode

Authentication & Cookies

Although we are not hitting the real server as we mocked our APIs, we may still have some code in our pages that checks whether the user is logged in or not, for example by checking if an auth cookie exists or not. If this is your case, then here is how to mock cookies:
In the same file where we mock the APIs, you need to add this function:
mock-apis.ts

/**
 * Mock the logged user session
 */
export async function mockLoggedUser(context: BrowserContext) {
    return context.addCookies([
        {
            name: 'auth_session',
            value: JSON.stringify({
                userId: 1,
                token: 'abcd',
            }),
            domain: 'localhost',
            path: '/',
            httpOnly: true,
            secure: true,
        },
    ]);
}

Enter fullscreen mode Exit fullscreen mode

Now you need to call this function inside your test. Here all the tests need an authenticated user, so we add it to a before hook instead of repeating it. If you need to fake authenticated users for one scenario only, then use it inside that specific test only, instead of the hook.
We should not forget to reset the mock at the end.
post.spec.ts

import { mockLoggedUser } from './mocks/api/mock-apis';

test.beforeEach(async ({ context }) => {
  mockLoggedUser(context);
});

// .... your tests

// Reset the mocks after each test
test.afterEach(async ({ next, context }) => {
  context.clearCookies();
});
Enter fullscreen mode Exit fullscreen mode

Post Requests & Server Actions

While dealing with post requests such as in forms for examples, you can:

Post in the Client

In case of making the request on the client, you can mock the request using normal playwright API mocks.

Server Actions

In this case the actual post request will happen on the server, you just trigger this from the client. If you want to know why you may need this, you can read about server actions from the official docs.

And in this particular case, the above mock configs should work fine. Just add the url to the mock function and fake a response.

In case the same url is used in several endpoints with different methods e.g. POST, GET, PUT, etc., you may need to check the method of the request as well:
mocks-api.ts

if (request.method === "POST" && request.url.endsWith('/posts')) {
    // Do something         
}
Enter fullscreen mode Exit fullscreen mode

Note: be sure that you await enough time in your tests after triggering the post action, otherwise you may get no response at all and your test terminates before the response fulfills, which gives you the illusion that your post mock is not working.

Remove TestIds

Although some test suites automatically configure your build to remove testids, here you need to configure this yourself, otherwise, you will ship to production markup that is bloated with unnecessary testids.
Here in next.config.ts we check if we are in production, then remove the data-testid attribute.
next.config.ts

const isProduction = process.env.NODE_ENV === "production";

module.exports = {
  compiler: {
    reactRemoveProperties: isProduction && { properties: ["^data-testid$"] },
  }
}
Enter fullscreen mode Exit fullscreen mode

Result

Make sure that the dev:test server is running, then run npm run test:e2e-ui if you added that alias to your package.json, and it finally works 🎉

passed tests result

Top comments (2)

Collapse
 
karimshalapy profile image
Karim Shalapy

Great Read!!

Collapse
 
weamadel profile image
Weam Adel

Thank you so much, Karim 🙏🏻