DEV Community

A0mineTV
A0mineTV

Posted on

Testing Next.js Server-Side Rendering APIs with Playwright and MSW

Server-side rendering (SSR) in Next.js can be tricky to test, especially when your pages depend on external APIs. In this article, we'll explore how to combine Playwright and Mock Service Worker (MSW) to create robust, reliable tests for your SSR pages.

The Challenge

When testing SSR pages that fetch data, you face several challenges:

  • External API dependencies make tests flaky
  • Network errors need to be simulated
  • Different data scenarios require different responses
  • Race conditions in parallel tests can cause inconsistent results

The Solution: Playwright + MSW

Playwright provides excellent end-to-end testing capabilities, while MSW intercepts network requests at the browser level, giving us complete control over API responses.

Setting Up the Testing Environment

1. Install Dependencies

npm install --save-dev @playwright/test msw
Enter fullscreen mode Exit fullscreen mode

2. Configure MSW in Your Next.js App

First, create MSW handlers for your API endpoints:

// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://jsonplaceholder.typicode.com/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'John Doe', username: 'johndoe', email: 'john@example.com' },
      { id: 2, name: 'Jane Smith', username: 'janesmith', email: 'jane@example.com' }
    ]);
  })
];
Enter fullscreen mode Exit fullscreen mode

3. Create Test Scenarios

Define different scenarios for your tests:

// src/mocks/scenarios.ts
import { http, HttpResponse } from 'msw';

export const scenarios = {
  default: handlers,

  apiError: [
    http.get('https://jsonplaceholder.typicode.com/users', () => {
      return new HttpResponse(null, { status: 500 });
    })
  ],

  emptyData: [
    http.get('https://jsonplaceholder.typicode.com/users', () => {
      return HttpResponse.json([]);
    })
  ],

  networkError: [
    http.get('https://jsonplaceholder.typicode.com/users', () => {
      return HttpResponse.error();
    })
  ]
};
Enter fullscreen mode Exit fullscreen mode

4. Set Up MSW API Route

Create a Next.js API route to control MSW during tests:

// src/app/api/msw/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { scenarios } from '@/mocks/scenarios';

let currentHandlers = scenarios.default;

export async function POST(request: NextRequest) {
  const { action, scenario } = await request.json();

  switch (action) {
    case 'useScenario':
      if (scenarios[scenario]) {
        currentHandlers = scenarios[scenario];
        return NextResponse.json({ message: `Scenario '${scenario}' applied` });
      }
      break;

    case 'reset':
      currentHandlers = scenarios.default;
      return NextResponse.json({ message: 'Handlers reset to default' });
  }

  return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
}
Enter fullscreen mode Exit fullscreen mode

Building Robust SSR Pages

Your SSR pages should handle errors gracefully:

// src/app/users/page.tsx
import Link from 'next/link';

async function getUsers() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users', {
    cache: 'no-store'
  });

  if (!res.ok) {
    throw new Error('Failed to fetch users');
  }

  return res.json();
}

export default async function UsersPage() {
  try {
    const users = await getUsers();

    return (
      <div className="container mx-auto px-4 py-8">
        <h1 className="text-3xl font-bold mb-6">Users</h1>
        <div className="grid gap-4">
          {users.map((user) => (
            <div key={user.id} className="border rounded-lg p-4">
              <h2 className="text-xl font-semibold mb-2">
                <Link href={`/users/${user.id}`}>
                  {user.name}
                </Link>
              </h2>
              <p className="text-gray-600">@{user.username}</p>
              <p className="text-gray-600">{user.email}</p>
            </div>
          ))}
        </div>
        <Link href="/" className="text-blue-600 hover:text-blue-800">
           Back to Home
        </Link>
      </div>
    );
  } catch (error) {
    return (
      <div className="container mx-auto px-4 py-8">
        <h1 className="text-3xl font-bold mb-6">Users</h1>
        <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
          <p className="text-red-800 font-semibold">Failed to fetch users</p>
          <p className="text-red-600 text-sm mt-2">
            {error instanceof Error ? error.message : 'An unknown error occurred'}
          </p>
        </div>
        <Link href="/" className="text-blue-600 hover:text-blue-800">
           Back to Home
        </Link>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Writing Playwright Tests

Test Setup

Create a test setup helper:

// tests/setup/test-setup.ts
import { test as baseTest } from '@playwright/test';

async function resetMSWHandlers() {
  const response = await fetch('http://localhost:3001/api/msw', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ action: 'reset' })
  });

  // Small delay to ensure reset is processed
  await new Promise(resolve => setTimeout(resolve, 100));
}

export const test = baseTest.extend({
  page: async ({ page }, use) => {
    await resetMSWHandlers(); // Reset before test
    await use(page);
    await resetMSWHandlers(); // Reset after test
  },
});

export async function useMSWScenario(scenario: string) {
  const response = await fetch('http://localhost:3001/api/msw', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ action: 'useScenario', scenario })
  });

  // Small delay to ensure scenario is applied
  await new Promise(resolve => setTimeout(resolve, 100));
}

export { expect } from '@playwright/test';
Enter fullscreen mode Exit fullscreen mode

Test Examples

// tests/ssr-parallel.spec.ts
import { test, expect, useMSWScenario } from './setup/test-setup';

test.describe('SSR with API Testing', () => {
  test('should display users successfully', async ({ page }) => {
    await page.goto('/users');

    await expect(page.locator('h1')).toContainText('Users');
    await expect(page.locator('text=John Doe')).toBeVisible();
    await expect(page.locator('text=Jane Smith')).toBeVisible();
  });

  test('should handle API errors gracefully', async ({ page }) => {
    await useMSWScenario('apiError');

    await page.goto('/users');

    // Verify error is displayed properly
    await expect(page.locator('h1')).toContainText('Users');
    await expect(page.locator('p.text-red-800:has-text("Failed to fetch users")')).toBeVisible();
    await expect(page.locator('.bg-red-50')).toBeVisible();
    await expect(page.locator('text=← Back to Home')).toBeVisible();
  });

  test('should handle empty data', async ({ page }) => {
    await useMSWScenario('emptyData');

    await page.goto('/users');

    await expect(page.locator('h1')).toContainText('Users');
    await expect(page.locator('text=John Doe')).not.toBeVisible();
  });

  test('should handle network errors', async ({ page }) => {
    await useMSWScenario('networkError');

    await page.goto('/users');

    await expect(page.locator('text=Failed to fetch users')).toBeVisible();
  });
});
Enter fullscreen mode Exit fullscreen mode

Playwright Configuration

Configure Playwright to avoid race conditions:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: false, // Prevent MSW race conditions
  workers: 1, // Single worker for MSW stability
  retries: 1,
  use: {
    baseURL: 'http://localhost:3001',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3001',
    reuseExistingServer: !process.env.CI,
    env: {
      NEXT_PUBLIC_API_MOCKING: 'enabled',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Key Benefits

Reliable Tests: No external API dependencies
Error Scenarios: Easy to test network failures and API errors
Fast Execution: No real network calls
Deterministic: Same results every time
Complete Control: Mock any API response scenario

Best Practices

  1. Reset MSW handlers before and after each test
  2. Add small delays after MSW operations to ensure they're processed
  3. Use sequential execution to avoid race conditions with shared MSW state
  4. Test error scenarios - they're as important as happy paths
  5. Keep handlers simple and focused on specific test scenarios

Conclusion

Combining Playwright with MSW provides a powerful testing setup for Next.js SSR applications. This approach gives you the confidence that your pages handle both successful API responses and various error conditions gracefully.

The key is proper isolation between tests and careful management of MSW state to avoid flaky tests. With this setup, you can build comprehensive test suites that cover all the edge cases your users might encounter.

Top comments (0)