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:
- Serverless Environment: Next.js API routes run in a serverless context that doesn't exist in Jest
- ES Modules: Many dependencies use ES modules that Jest struggles to parse
- Mixed Environments: Components run in the browser, API routes run on the server
- 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.