This repository illustrates how to test React Server Components in Next.js with MSW.
Important
This example is work-in-progress. It relies on beta features and is subjected to change. Please do not use it in production just yet. Follow the implementation progress here.
Follow the Node.js integration of MSW to enable it for server-side requests in Next.js.
This is achieved by adding this to your layout.tsx
component:
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { server } = await import('@/mocks/node')
server.listen({
remote: {
enabled: true,
},
})
}
Set the remote.enabled
option to true
on server.listen()
to instruct MSW that there will be a remote server responsible for handling outgoing server-side requests.
In order to test RSCs reliably and have proper test and network isolation, we will be spawning a new instance of this Next.js app for each such test.
Define an app launcher using the @epic-web/app-launcher
package:
// tests/utils.ts
import { defineLauncher } from '@epic-web/app-launcher'
import getPort from 'get-port'
import { remoteContext } from 'msw/node'
// Define a launcher for a Next.js application.
export const launcher = defineLauncher<{ port: number }>({
async context() {
return {
// Run the application on a random port.
port: await getPort(),
}
},
env() {
return {
// Associate this application instance with the surrounding
// `remote.boundary()` closure.
[remoteContext.variableName]: remoteContext.getContextId(),
}
},
command({ context }) {
// Provide the random port to the actual run command.
return `npm start -- --port ${context.port}`
},
url({ context }) {
// Return a URL including the random port so the launcher
// would wait until the app process is running at this URL.
return new URL(`https://localhost:${context.port}`)
},
})
Use get-port
to procure a random vacant port, and remoteContext
from MSW to bind the test closure to this particular application instance.
Finally, let's write some tests. But before that, make sure to build the application once before the test run:
// playwright.global.ts
import { spawnSync } from 'node:child_process'
export default function globalSetup() {
spawnSync('npm', ['run', 'build'], { stdio: 'inherit' })
}
// playwright.config.ts
export default defineConfig({
// ...
globalSetup: './playwright.global.ts',
})
Now, use the launcher
you've defined earlier to spawn your Next.js application at a random port in tests:
// tests/homepage.test.ts
test('renders the user greeting', async ({ page }) => {
await using app = await launcher.run()
await page.goto(app.url.href)
await expect(page.getByText('Welcome, John Maverick!')).toBeVisible()
})
We are almost done. The setup and the test are ready, but now you need to define request handlers.
Start by calling setupRemoteServer()
from msw/node
and running that remote server in the beforeAll
and afterAll
hooks in Playwright:
// tests/homepage.test.ts
import { http } from 'msw'
import { setupRemoteServer } from 'msw/node'
const remote = setupRemoteServer()
test.beforeAll(async () => {
await remote.listen()
})
test.afterEach(() => {
remote.resetHandlers()
})
test.afterAll(async () => {
await remote.close()
})
// ...
Note that
.listen()
and.close()
methods are asynchronous.
Then, wrap the test cases in remote.boundary()
to isolate request handlers, allowing you to run your tests in parallel without experiencing the shared state issue.
// tests/homepage.test.ts
// ...
test('renders the user greeting', async ({ page }) => {
await remote.boundary(async () => {
remote.use(
// Describe the network of the server-side Next.js.
http.get('https://api.example.com/user', () => {
return Response.json({ name: 'John Maverick' })
})
)
await using app = await launcher.run()
await page.goto(app.url.href)
await expect(page.getByText('Welcome, John Maverick!')).toBeVisible()
})()
})
Unfortunately, Playwright doesn't allow wrapping the test callback in a function. For the time being, you can rely on self-invoked
remote.boundary()
.
Done! 🎉
You can run the tests to see MSW intercept and handle requests in your Server Components from within the test suite:
npm test