Back to Blog
January 19, 2025

Why Your Next.js Tests Are Failing (And How to Fix Them)

The Problem

If you've ever written tests for a Next.js application, you've probably encountered some frustrating errors:

  • Request is not defined
  • Unexpected token 'export'
  • Tests that pass locally but fail in CI
  • Mocks that work in one test but break another

These aren't just random issues—they're symptoms of a deeper problem with how we approach testing in the Next.js ecosystem.

The Root Cause

Next.js applications have unique characteristics that make traditional testing approaches fail:

  1. Serverless Environment: Next.js API routes run in a serverless context that doesn't exist in Jest
  2. ES Modules: Many dependencies use ES modules that Jest struggles to parse
  3. Mixed Environments: Components run in the browser, API routes run on the server
  4. Global State: Next.js has global objects that don't exist in the test environment

The Solution: Proper Mocking Strategy

The key insight is that every external dependency must be mocked. This isn't just about avoiding network requests—it's about creating a controlled, predictable test environment.

The Golden Rule: Mock Everything

// ❌ DON'T: Let tests make real requests
it('should fetch data', async () => {
  const response = await fetch('/api/data');
  expect(response.ok).toBe(true);
});

// ✅ DO: Mock all external dependencies
global.fetch = jest.fn();
(global.fetch as jest.Mock).mockResolvedValue({
  ok: true,
  json: () => Promise.resolve({ data: 'test' })
});

The Next.js API Route Challenge

API routes are particularly tricky because they use objects that don't exist in the Node.js test environment:

// This will fail in tests
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const data = await request.json();
  return NextResponse.json({ success: true });
}

The Fix: Mock NextResponse

// Always mock NextResponse before importing your routes
jest.mock('next/server', () => ({
  NextRequest: class NextRequest {
    url: string;
    method: string;
    headers: Headers;
    body: any;

    constructor(input: string | Request, init?: RequestInit) {
      if (typeof input === 'string') {
        this.url = input;
      } else {
        this.url = input.url;
      }
      
      this.method = init?.method || 'GET';
      this.headers = new Headers(init?.headers);
      this.body = init?.body;
    }

    json() {
      return Promise.resolve(this.body ? JSON.parse(this.body as string) : {});
    }
  },
  NextResponse: {
    json: jest.fn((data, init) => ({
      status: init?.status || 200,
      json: () => Promise.resolve(data)
    }))
  }
}));

The ES Module Problem

Many modern packages use ES modules, which Jest can't parse:

// This will fail with "Unexpected token 'export'"
import { Queue } from 'bullmq';

The Fix: Mock ES Modules

// Mock BullMQ to prevent ES module issues
jest.mock('bullmq', () => ({
  Queue: jest.fn().mockImplementation(() => ({
    add: jest.fn().mockResolvedValue({ id: 'test-job-id' }),
    getJob: jest.fn().mockResolvedValue({
      id: 'test-job-id',
      data: {},
      getState: jest.fn().mockResolvedValue('completed')
    })
  })),
  Worker: jest.fn().mockImplementation(() => ({
    on: jest.fn(),
    run: jest.fn()
  }))
}));

The Test Structure Pattern

Every test should follow this pattern:

import '@testing-library/jest-dom';

// 1. Mock fetch globally first
global.fetch = jest.fn();

// 2. Mock external modules
jest.mock('fs/promises');
jest.mock('next/navigation');

// 3. Import your code
import { GET } from '@/app/api/your-route/route';

describe('Your API Route', () => {
  beforeEach(() => {
    // 4. Clear all mocks before each test
    jest.clearAllMocks();
    (global.fetch as jest.Mock).mockClear();
  });

  it('should work', async () => {
    // 5. Setup specific mocks for this test
    (global.fetch as jest.Mock).mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({ data: 'test' })
    });

    // 6. Execute and assert
    const response = await GET(new NextRequest('http://localhost/api/test'));
    expect(response.status).toBe(200);
  });
});

The Journey Ahead

This is just the beginning. In the next posts, we'll explore:

  • Post 2: The Global Mock Trap (and how to avoid it)
  • Post 3: Testing Real Services vs. Mocked Services
  • Post 4: Component Testing Patterns for Next.js
  • Post 5: Advanced Mocking Strategies

The key takeaway: Next.js testing requires a different mindset. It's not about testing in isolation—it's about creating a controlled environment where your code can run predictably.

Quick Checklist

Before writing any Next.js test, ask yourself:

  • [ ] Have I mocked global.fetch?
  • [ ] Have I mocked NextResponse for API routes?
  • [ ] Have I mocked ES modules like BullMQ?
  • [ ] Am I clearing mocks in beforeEach?
  • [ ] Am I testing the real implementation, not a mock?

Remember: When in doubt, mock it out!


Next up: We'll dive deeper into the global mock trap and why it's the #1 cause of brittle tests.

nextjsunit-testingjestmockingapi-routeses-modulestesting-patternsserverlessnext-responsebullmq