Achieve Blazing-Fast Next.js SSR API Tests with Parallel Playwright and MSW
Are your Next.js Server-Side Rendering (SSR) API tests slowing down your development cycle? If you’re using Playwright and Mock Service Worker (MSW), you might have encountered the challenge of sequential test execution due to MSW’s global state. This article will guide you through a powerful revision that unlocks true parallel testing, significantly accelerating your test suite and boosting your productivity!
The Challenge: Sequential Testing with MSW
Previously, integrating Playwright with MSW for Next.js SSR API tests often led to a bottleneck: MSW’s global state meant that only one test could run at a time without interfering with others. This sequential execution, while functional, became a major drag as test suites grew, negating the performance benefits of modern testing frameworks.
The Solution: Dynamic Parallelism
We’ve completely overhauled the testing setup to enable robust parallel execution. The core idea is to move away from a single, globally-managed mock server and instead dynamically provision multiple, isolated mock servers, one for each parallel test worker. This ensures that each Playwright instance has its own dedicated MSW environment, eliminating state conflicts and allowing tests to run concurrently.
Here’s a summary of the key revisions that make this possible:
-
Dynamic Mock Server Spawning (
mock-server.ts
): Instead of a single mock server, ourmock-server.ts
now dynamically spawns multiple server instances. The number of parallel mock servers is controlled by theMOCK_SERVER_COUNT
environment variable in your.env
file, allowing you to scale up or down based on your system’s resources. -
Parallel Playwright Configuration (
playwright.config.ts
): We’ve configured Playwright to leverage itsfullyParallel: true
setting and dynamically assign mock server ports to each test worker. This is achieved by usingtest.info().workerIndex
to calculate a unique port for each parallel test. -
Dynamic API Paths (
server.ts
,handlers.ts
,page.tsx
): To support multiple mock servers running on different ports, the API paths within our Next.js application (specifically inpage.tsx
) and the MSW handlers (handlers.ts
) now dynamically accept the mock server port as a parameter. This ensures that each test communicates with its designated mock server. -
Scalable Performance: With
MOCK_SERVER_COUNT
, you have the power to easily adjust the number of concurrent mock servers. This means you can maximize your parallel test execution based on your development machine’s capabilities, leading to significantly faster feedback loops.
Step-by-Step Implementation Guide
Let’s walk through how to set up this powerful parallel testing environment.
1. Project Setup
If you haven’t already, initialize your Next.js project and install the necessary dependencies:
-
Create Next.js App:
npx create-next-app@latest my-app --yes cd my-app
-
Install Playwright:
npm init playwright@latest
-
Install MSW:
npm i msw --save-dev
-
Install
tsx
: Simplifies running TypeScript files directly without complextsconfig.json
setups for mocks.npm install -D tsx
-
Install
dotenv
: To load environment variables from your.env
file.npm install --save-dev dotenv
2. Configure Environment Variables (.env
)
Create a .env
file in your project root and define the number of mock servers you want to run in parallel. A good starting point is often the number of CPU cores you have.
MOCK_SERVER_COUNT=6
3. Update src/app/page.tsx
Modify your main page component to accept a mockPort
as a search parameter. This port will tell your Next.js app which specific mock server to fetch data from.
(Excerpt from page.tsx
showing the key change)
export default async function Home({ searchParams }: { searchParams?: Promise<{ mockPort?: string }> }) {
try {
const params = await searchParams;
// Get the mock port from URL search params (set by tests) or default to 3001
const mockPort = params?.mockPort || '3001';
// Fetch Pokemon data from our mock API using the dynamic port
const response = await fetch(`http://localhost:${mockPort}/api/v2/pokemon/charizard`);
// ... rest of your component logic
} catch (error) {
// ... error handling
}
}
4. Define MSW Handlers (mocks/handlers.ts
)
Create or update mocks/handlers.ts
to include your mock data and a createHandlers
factory function that takes a port
as an argument. This ensures that MSW can intercept requests on specific ports.
(Excerpt from handlers.ts
showing the key change)
import { http, HttpResponse } from "msw";
export const mockData = { /* ... your mock pokemon data ... */ };
export const createHandlers = (port: number) => [
http.get(`http://localhost:${port}/api/v2/pokemon/charizard`, () => {
console.log(`MSW[${port}]: → returning charizard data (default handler)`);
return HttpResponse.json(mockData.charizard.data);
}),
// ... other mock handlers using the 'port' variable
];
5. Create the Dynamic Mock Server (mock-server.ts
)
This is the central piece for managing parallel MSW instances. Create mock-server.ts
in your project root. This script will read MOCK_SERVER_COUNT
and either spawn multiple child processes, each running a single MSW instance on a unique port, or run a single instance if MOCK_SERVER_COUNT
is 1.
(Excerpt from mock-server.ts
illustrating the multi-server logic)
import { spawn } from 'child_process';
import { config } from 'dotenv';
// ... other imports
config();
const mockServerCount = parseInt(process.env.MOCK_SERVER_COUNT || '1');
if (mockServerCount > 1) {
const basePort = 3001;
console.log(`Starting ${mockServerCount} mock servers...`);
for (let i = 0; i < mockServerCount; i++) {
const port = basePort + i;
// Spawn child process for each mock server
const child = spawn('npx', ['tsx', 'mock-server.ts', port.toString()], {
stdio: 'inherit',
shell: true,
env: { ...process.env, MOCK_SERVER_COUNT: '1' } // Important: tell child processes to run in single-server mode
});
// ... handle child process
}
} else {
// Single server mode: Logic to start a single MSW server and its HTTP bridge
// ... (see full code for details)
}
6. Configure Playwright (playwright.config.ts
)
Modify your playwright.config.ts
to enable full parallelism, load environment variables, and dynamically set the number of workers based on MOCK_SERVER_COUNT
.
import { defineConfig, devices } from '@playwright/test';
import dotenv from "dotenv";
import path from "path";
// Load environment variables from .env file
dotenv.config({ path: path.resolve(__dirname, ".env") });
export default defineConfig({
testDir: './tests',
fullyParallel: true, // Enable fully parallel tests
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: parseInt(process.env.MOCK_SERVER_COUNT || "4"), // Use MOCK_SERVER_COUNT for workers
reporter: 'html',
use: {
baseURL: 'http://localhost:3000', // Your Next.js app's URL
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// Comment out or remove other browsers if you only want to test in Chromium for now
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
],
webServer: {
command: 'npm run dev', // Command to start your Next.js app
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
7. Write Your Playwright Tests (tests/example.spec.ts
)
Your Playwright tests will now use a helper function to communicate with the appropriate mock server based on the workerIndex
and navigate to your Next.js page with the mockPort
search parameter.
(Excerpt from example.spec.ts
showing the key helper function)
import { test, expect } from "@playwright/test";
const setMockPokemon = async (page: any, mockType: "pikachu" | "eevee" | "error500" | null) => {
const pokemon = mockType || "charizard";
// Calculate port based on worker index for parallel testing
const port = 3001 + test.info().workerIndex; // Base port + worker index
// Send a request to our mock server to switch the Pokemon data on its specific port
await page.request.post(`http://localhost:${port}/api/switch-pokemon`, {
data: { pokemon },
});
// Navigate to the home page with the dynamic mock port parameter
await page.goto(`/?mockPort=${port}`);
await page.waitForLoadState("networkidle");
};
test.describe("Pokemon Basic Tests", () => {
test("Charizard (Default Pokemon)", async ({ page }) => {
await setMockPokemon(page, null);
await expect(page.locator("h1")).toContainText("Charizard");
// ... assertions and screenshot
});
// ... other parallel tests for different Pokemon or error states
});
8. Run Your Tests
Open three separate terminal windows:
-
Terminal 1: Start Mock Servers
npx tsx mock-server.ts
-
Terminal 2: Start Next.js Application
npm run dev
-
Terminal 3: Run Playwright Tests
npx playwright test
You'll observe your Playwright tests executing in parallel, each interacting with its isolated mock server instance!
Outro: Faster Feedback, Happier Developers
By adopting this parallel testing strategy, you've transformed your Next.js SSR API testing from a sequential crawl to a blazing-fast sprint. The ability to run Playwright tests concurrently with isolated MSW environments dramatically reduces overall test execution time, provides quicker feedback during development, and ultimately leads to a more efficient and enjoyable coding experience. While continuous learning in testing is always key, this setup provides a robust foundation for high-performance frontend API testing.
Thank you for reading, and happy AI coding! 🤖