End-to-End testing of a Next.js application using Playwright and MSW
Server and Client Components in Next.js
Before writing end-to-end tests, let’s first understand how Next.js works and what specific features it introduces - namely, the concept of server and client components.
In the past, we typically had only client-side JavaScript and separate code that ran on the server. Next.js revolutionized the frontend world by merging these two concepts into a unified system, allowing developers to seamlessly transition between components. The only real distinction now lies in explicitly declaring «use client» at the top of files that need to run in the browser. This can become a source of confusion if you're not familiar with how both types of components work.
By default, all components in Next.js are server components and are rendered on the server. This comes with both advantages and limitations. For example, server components can access data directly, work with environment variables, and securely use access tokens. On the flip side, they cannot use React hooks, trigger interactive client-side events, or access browser APIs.
How to check where a component executes?
The simplest way is to use console.log
: add a log inside your component and see where it shows up - either in the browser console or in the server terminal.
Why is it important to understand the difference between component types?
Because when writing tests, we now need to mock requests coming from both the server and the client sides separately. Each type of component may initiate its own set of data fetches, and correctly handling them in tests requires a clear understanding of where the logic is executed.
Writing mocks for server and client
For mocking, we’ll use the msw library, as it’s simple, well-documented, and widely adopted. Additionally, we’ll use playwright-msw to avoid manually setting up the integration with playwright.
Mocking the client side
We’ll start by mocking client-side requests in the mocks/clientHandlers.ts file:
import { http, HttpResponse } from 'msw';
import { mockData } from '@/mocks/data';
const backendApiUrl = 'http://127.0.0.1:8000';
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE',
'Access-Control-Allow-Headers': '*',
};
export const handlers = [
http.get(`${backendApiUrl}/api/data`, () => {
return HttpResponse.json(
{ success: true, data: mockData },
{ headers }
);
}),
];
You may notice there's also a mocks/data.ts file containing our mock data, which needs to be prepared in advance.
Next, let’s create a worker for the client in mocks/client.ts:
import { setupWorker } from 'msw/browser';
import { handlers } from './clientHandlers';
export const worker = setupWorker(...handlers);
Mocking the server side
Similar to the client-side setup, the server side will also consist of two files: one for defining mocked request handlers (mocks/serverHandlers.ts) and another for setting up the server (mocks/mockServer.ts).
The serverHandlers.ts
file will look similar to the client-side version, with the key difference being that it only mocks requests initiated from the server side. Some handlers might overlap with client ones, but that's not a requirement.
The server (mocks/mockServer.ts) will look like this:
import { createServer } from '@mswjs/http-middleware';
import { handlers } from './serverHandlers';
const httpServer = createServer(...handlers);
httpServer.listen(8000, () => {
console.log('started mock server on port 8000');
});
Here, I’m using the @mswjs/http-middleware library to spin up a mock server that will intercept server-side requests. You can start this server with the following command:
npx tsx mocks/mockServer.ts
This server will be useful when running our tests.
Integration with Playwright
Playwright has solid documentation, so we won’t go into detail about how to install, configure, or write basic tests - you’ll most likely refer to the playwright.config.ts file and adjust it to your needs.
What’s more interesting is how to connect Playwright with our client-side mock server. To do that, we create a file e2e/test.ts where we register our MSW workers:
import { test as base, expect } from '@playwright/test';
import { http } from 'msw';
import type { MockServiceWorker } from 'playwright-msw';
import { createWorkerFixture } from 'playwright-msw';
import { handlers } from '@/mocks/clientHandlers';
const test = base.extend<{ worker: MockServiceWorker; http: typeof http; }>({
worker: createWorkerFixture(handlers),
http,
});
export { expect, test };
Next, in the tests, we will use these extended functions:
import { expect, test } from '@/e2e/test';
Running the tests
At this point, the setup is complete. Now, we just need to run the tests. First, build the project (in the production environment):
npm run build
Next, start our server for server-side requests:
npx tsx mocks/mockServer.ts
As we remember, client-side requests are intercepted by the workers within the tests. Finally, run the tests:
npx playwright test
I personally prefer the feature - rich UI mode, which allows running individual tests, watching their execution, quickly catching errors (both console and request-related), understanding the order in which the page is rendered, what requests are sent, what responses are returned, and much more:
npx playwright test --ui
I highly recommend checking it out, it's an invaluable tool when writing tests and debugging.
Authentication in the application
One of the essential aspects you'll encounter when writing tests is authentication in the application. It's nice that Playwright doesn't ignore this or leave developers to write their own workarounds; instead, it provides a solid guide. So, there shouldn’t be any issues with this.
The importance of accessibility
Before writing tests, it’s crucial to check your application for accessibility. This isn’t just a whim of people with disabilities, and it shouldn’t be seen as merely a matter of good manners. First and foremost, it’s about the convenience of all users. Keyboard navigation, clear messages, and predictable behavior of design elements - these all make the application easier to use and help users solve the problem they came to address more efficiently.
What does this have to do with tests, you might ask? Well, when selecting elements, checking text, and generally ensuring the app's functionality, it's important to account for ARIA attributes. Playwright itself recommends prioritizing more explicit indicators for finding elements, rather than relying on implicit ones like CSS classes, though that’s also possible. This way, we are once again testing that the application is accessible and clear to the user.
Writing tests
First and foremost, explore the capabilities of Playwright: how to select elements, how to verify results, and check out best practices.
To avoid code duplication, use page object models and fixtures. Learn Playwright's features so that your tests are clear, simple, and concise.
These tests serve as your documentation, write them thoughtfully.
Plugin for VSCode
It’s clear that end-to-end testing often involves repetitive tasks: selecting an element, clicking it, filling in a field, and so on. Don’t be afraid to automate everything you can.
What I like about Playwright is that it doesn’t just provide a VSCode plugin, but also explains how to use it. And it can really save you a lot of time. Its interactive test recording mode is a real time-saver! You simply start the recording mode, and everything you click in the browser will be recorded as test code: every selected element, every interaction. All you’ll have to do is organize everything, refactor repetitive parts into models, and write the assertions!
Never has writing tests been so enjoyable! My love at first click! :)
Working with time zones
As with the previous cases, you need to set the time zone for both the server and the client.
For the server, add the following to the playwright.config.ts file:
export default defineConfig({
use: {
timezoneId: 'Europe/Paris',
},
});
For the client, set the desired time zone when starting the frontend:
TZ='Europe/Paris' npx playwright test --ui
Conclusion
The hardest part of writing tests is getting started. Setting up the environment, figuring out how everything works, and understanding where and what to mock - this really takes some time. You also need to prepare test data.
But after the first test, you realize how well the application is designed and how easy it is to write tests. Playwright has done everything to make writing tests as fast and easy as possible.
I definitely recommend it. So far, it's a 10 out of 10. All the scenarios I encountered could be covered using the existing tools, without workarounds or additional plugins. Some features, like automatic test recording, even pleasantly surprised me.
Комментарии