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
Here I named my e2e testing directory to e2e instead of the default naming.
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
This command runs the tests in an non-interactive mode, so like jest you will get the results in the terminal as follows.
Test Interactive UI
npx playwright test --ui
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:
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"
}
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.
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
And in your next.config.ts
file:
module.exports = {
experimental: {
testProxy: true,
},
}
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"
}
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,
},
});
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.
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."
}
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);
});
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");
}
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,
},
]);
}
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();
});
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
}
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$"] },
}
}
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 🎉
Top comments (2)
Great Read!!
Thank you so much, Karim 🙏🏻