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:

  1. Dynamic Mock Server Spawning (mock-server.ts): Instead of a single mock server, our mock-server.ts now dynamically spawns multiple server instances. The number of parallel mock servers is controlled by the MOCK_SERVER_COUNT environment variable in your .env file, allowing you to scale up or down based on your system’s resources.
  2. Parallel Playwright Configuration (playwright.config.ts): We’ve configured Playwright to leverage its fullyParallel: true setting and dynamically assign mock server ports to each test worker. This is achieved by using test.info().workerIndex to calculate a unique port for each parallel test.
  3. 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 in page.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.
  4. 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 complex tsconfig.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! 🤖

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.
You need to agree with the terms to proceed