When you're writing end-to-end tests for your Next.js application, there might come a time when you want to mock HTTP requests that your app is making.
Maybe your app is interacting with a third-party API and its output isn't predictable. Or maybe you want to test how your app behaves when the server returns an error.
Historically, this has been a bit tricky for Next.js apps where requests can be made from the browser or the server.
My buddy Max (yes, another Max Schmitt – don't get confused!) recently published an experimental package for Playwright that makes it super-easy to mock server-side requests.
Here's an example of how it works:
An Example Page
Let's say we have a simple Next.js project (using the app router) to display my last 3 repositories on GitHub:
app/page.tsx
export default async function Home() {const repos = await fetch('https://api.github.com/users/maximilianschmitt/repos?sort=updated', {// It's important to use cache: 'no-cache', otherwise we can't// mock the request. See:// https://github.com/playwright-community/ssr/issues/7cache: 'no-cache',}).then((res) => res.json()).then(repos => repos.slice(0, 3))return (<main><h1>Latest repositories</h1><ul>{repos.map((repo: any) => {return <li key={repo.id}>{repo.full_name}</li>})}</ul></main>)}
In the browser, the page would look something like this:
An Example Playwright Test
If we wanted to test this page, we could write the following test with Playwright:
app/page.test.ts
import { test, expect } from 'playwright'test('displays latest repos from GitHub', async ({ page }) => {// Visit the homepageawait page.goto('/')// Check that the correct title is displayedawait expect(page.locator('h1')).toContainText('Latest repositories')// Check that we have exactly 3 reposawait expect(page.locator('li')).toHaveCount(3)await expect(page.locator('li').nth(0)).toContainText('maximilianschmitt/nextjs-playwright-ssr')await expect(page.locator('li').nth(1)).toContainText('maximilianschmitt/nextjs-contact-form-app')await expect(page.locator('li').nth(2)).toContainText('maximilianschmitt/nextjs-contact-form-pages')})
Now, what happens if I create new repositories? I would need to update the tests!
To avoid this, let's mock the server-side request and return a fixed list of repositories.
Mocking Next.js' Server-Side Requests
We'll use the experimental playwright-ssr package to mock the server-side requests.
Let's update our test file:
app/page.test.ts
// Import 'test' and 'expect' from 'playwright-ssr'// instead of 'playwright' 👇import { test, expect } from 'playwright-ssr'// Get a reference to the webServer fixture 👇test('displays latest repos from GitHub', async ({ page, webServer }) => {// Intercept the request to https://api.github.com/users/maximilianschmitt/repos?sort=updated// and reply with our own list of reposawait webServer.route('https://api.github.com/users/*/repos*', async (route) => {await route.fulfill({status: 200,json: [{ id: 1, full_name: 'maximilianschmitt/hello-world' },{ id: 2, full_name: 'maximilianschmitt/maxschmitt.me' },{ id: 3, full_name: 'maximilianschmitt/cakedesk' },],})})// Visit the homepageawait page.goto('/')// ...})
Isn't this super easy? I really love how easy it is to describe which request we want to intercept and what response we want to send back.
How to Setup playright-ssr
How do we set this up? It's pretty easy!
First, install playwright-ssr
:
npm install playwright-ssr
Then, adjust the playwright.config.ts
file like so:
playwright.config.ts
import { defineConfig, devices } from '@playwright/test'import { WorkerConfigOptions } from 'playwright-ssr'export default defineConfig<WorkerConfigOptions>({// ...projects: [{name: 'chromium',use: {...devices['Desktop Chrome'],webServer: {command: 'npm',args: ['run', 'dev'],url: 'http://localhost:3000',cwd: __dirname,},},},],})
And that's it!
Check out the playwright-ssr project on GitHub. You can find the full example from this post on GitHub as well.