The Global Mock Trap: Why Your Tests Are Brittle
The Problem with Global Mocks
If you've ever written tests for a Next.js application, you've probably encountered this pattern:
// In jest.setup.ts
jest.mock('@/services/gitlab', () => ({
default: jest.fn().mockImplementation(() => ({
getVersionTags: jest.fn().mockResolvedValue([]),
getVersionFiles: jest.fn().mockResolvedValue([])
}))
}));
jest.mock('@/utils/debug', () => ({
debugLog: jest.fn(),
debugError: jest.fn()
}));
It seems convenient at first—set up your mocks once and forget about them. But this approach creates a global mock trap that makes your tests brittle, unpredictable, and hard to maintain.
Why Global Mocks Are Dangerous
1. Testing the Mock, Not the Real Code
When you mock a service globally, you're not testing the actual service implementation. You're testing a mock that might not behave like the real service.
// ❌ WRONG: Testing the mock, not the real service
jest.mock('@/services/gitlab', () => ({
default: jest.fn().mockImplementation(() => ({
getVersionTags: jest.fn().mockResolvedValue([])
}))
}));
// This creates a mock service, not the real one
const service = new GitLabService({...}); // Uses mock, not real service
2. Interference Between Tests
Global mocks create shared state that can leak between tests, causing unpredictable behavior:
// Test 1: Sets up a mock
jest.mock('@/services/gitlab', () => ({
default: jest.fn().mockImplementation(() => ({
getVersionTags: jest.fn().mockResolvedValue(['v1.0.0'])
}))
}));
// Test 2: Expects different behavior but gets Test 1's mock
it('should handle empty results', async () => {
const service = new GitLabService({...});
const tags = await service.getVersionTags();
expect(tags).toEqual([]); // ❌ Fails because Test 1's mock is still active
});
3. Inability to Test Real Implementation
Global mocks prevent you from testing the actual service logic:
// ❌ WRONG: Can't test real service behavior
jest.mock('@/services/gitlab', () => ({
default: jest.fn().mockImplementation(() => ({
getVersionTags: jest.fn().mockResolvedValue([])
}))
}));
// This test can't verify the real service works correctly
it('should handle API errors', async () => {
const service = new GitLabService({...});
// Can't test real error handling because we're using a mock
});
The Right Approach: Individual Test Mocks
Instead of global mocks, use individual test mocks that give you precise control:
Service Testing Pattern
import '@testing-library/jest-dom';
// ALWAYS mock fetch globally first
global.fetch = jest.fn();
// Mock the debug utility (since we're not testing it)
jest.mock('@/utils/debug', () => ({
debugLog: jest.fn(),
debugError: jest.fn()
}));
// Import the REAL service (not mocked)
import GitLabService from '@/services/gitlab';
describe('GitLabService', () => {
let gitlabService: GitLabService;
beforeEach(() => {
// ALWAYS clear all mocks before each test
jest.clearAllMocks();
(global.fetch as jest.Mock).mockClear();
// Create REAL service instance
gitlabService = new GitLabService({
baseUrl: 'https://gitlab.com',
token: 'mock-token'
});
});
it('should fetch repository files', async () => {
// SETUP MOCKS FIRST
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: () => Promise.resolve([
{ name: 'test.sql', path: 'sql/test.sql' }
])
});
// EXECUTE (real service method)
const files = await gitlabService.getVersionFiles('MySql', '2024.1');
// ASSERT expected behavior
expect(files).toHaveLength(1);
expect(files[0].name).toBe('test.sql');
expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining('/repository/tree'),
expect.objectContaining({
headers: expect.objectContaining({
'PRIVATE-TOKEN': 'mock-token'
})
})
);
});
});
Utility Testing Pattern
import '@testing-library/jest-dom';
// Import the REAL utility functions (not mocked)
import { debugLog, debugError } from '@/utils/debug';
describe('Debug Utility', () => {
beforeEach(() => {
// ALWAYS clear all mocks before each test
jest.clearAllMocks();
// Mock console methods locally
console.log = jest.fn();
console.error = jest.fn();
// Reset environment
delete process.env.DEBUG;
});
it('should log message when DEBUG is true', () => {
// SETUP MOCKS FIRST
process.env.DEBUG = 'true';
// Mock Date.toISOString for predictable timestamps
const mockTimestamp = '2024-01-01T00:00:00.000Z';
jest.spyOn(Date.prototype, 'toISOString').mockReturnValue(mockTimestamp);
// EXECUTE (real utility function)
debugLog('test message', { key: 'value' });
// ASSERT expected behavior
expect(console.log).toHaveBeenCalledWith(
'[2024-01-01T00:00:00.000Z]',
'test message',
{ key: 'value' }
);
});
});
When Global Mocks Are Acceptable
There are specific cases where global mocks are necessary:
ES Module Compatibility Issues
// ✅ ACCEPTABLE: BullMQ has 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()
}))
}));
Next.js Environment Compatibility
// ✅ ACCEPTABLE: NextResponse doesn't exist in Node.js test environment
jest.mock('next/server', () => ({
NextResponse: {
json: jest.fn((data, init) => ({
status: init?.status || 200,
json: () => Promise.resolve(data)
}))
}
}));
CSS/Asset Parsing Issues
// ✅ ACCEPTABLE: PrimeReact CSS causes parsing errors
jest.mock('primereact/password/Password.css', () => ({}));
The Global Mock Trap in Action
Here's a real example of how global mocks can break your tests:
Before (Broken with Global Mock)
// In jest.setup.ts - GLOBAL MOCK
jest.mock('@/services/gitlab', () => ({
default: jest.fn().mockImplementation(() => ({
getVersionTags: jest.fn().mockResolvedValue([])
}))
}));
// Test file
describe('GitLabService', () => {
it('should work', async () => {
const service = new GitLabService({...}); // Uses mock, not real service
const tags = await service.getVersionTags(); // Calls mock method
expect(tags).toEqual([]); // Always passes, but we're not testing real code
});
});
After (Working with Individual Mocks)
// Test file - INDIVIDUAL MOCKS
global.fetch = jest.fn(); // Mock dependency
import GitLabService from '@/services/gitlab'; // Real service
describe('GitLabService', () => {
beforeEach(() => {
jest.clearAllMocks();
(global.fetch as jest.Mock).mockClear();
});
it('should work', async () => {
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: () => Promise.resolve([...])
});
const service = new GitLabService({...}); // Real service
const tags = await service.getVersionTags(); // Real method
expect(tags).toEqual([...]); // Tests actual behavior
});
});
Key Principles
1. Test the Real Thing, Mock the Dependencies
// ✅ DO: Test real service with mocked network
const service = new GitLabService({...}); // Real service
(global.fetch as jest.Mock).mockResolvedValue({...}); // Mocked dependency
// ❌ DON'T: Mock the service itself
jest.mock('@/services/gitlab', () => ({...})); // Don't mock what you're testing
2. Mock at the Right Level
// ✅ DO: Mock at the boundary (network, file system, etc.)
global.fetch = jest.fn();
jest.mock('fs/promises');
// ❌ DON'T: Mock internal business logic
jest.mock('@/services/gitlab'); // Don't mock the service you're testing
3. Clear Mocks Between Tests
beforeEach(() => {
// ALWAYS clear all mocks before each test
jest.clearAllMocks();
(global.fetch as jest.Mock).mockClear();
});
The Journey Continues
Understanding the global mock trap is crucial for writing reliable tests. In the next posts, we'll explore:
- Post 3: Testing Real Services vs. Mocked Services (Deep Dive)
- Post 4: Component Testing Patterns for Next.js
- Post 5: Advanced Mocking Strategies and Best Practices
Quick Checklist
Before writing any test, ask yourself:
- [ ] Am I testing the real implementation or a mock?
- [ ] Am I mocking dependencies, not the code I'm testing?
- [ ] Are my mocks isolated to individual tests?
- [ ] Am I clearing mocks between tests?
- [ ] Do I understand what each mock is doing?
Remember: Global mocks are a trap. Individual mocks give you control.
Next up: We'll dive deeper into testing real services and why it matters for your application's reliability.