Back to Blog
February 24, 2025

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.

testingjestmockingnextjsunit-testingtest-patternsjavascripttypescriptbest-practicestest-reliability