Module Mocking

The

module mocking utilities provide a robust solution for mocking entire modules in Bun tests. These utilities work around known issues in bun:test where mock.restore() doesn't properly restore modules that were mocked with mock.module().

Important: Bun has a known issue (#7823) where module mocks aren't properly restored. This library provides utilities to work around this limitation.

createModuleMocker

Signature

function createModuleMocker(): {
  mock: (modulePath: string, mockImplementation: () => MockedModule) => Promise<void>;
  restore: (modulePath: string) => void;
  restoreAll: () => void;
}

Description

Creates a module mocker instance that properly handles module mocking and restoration. This is the primary utility for managing module mocks in your test suites.

Methods

mock(modulePath, mockImplementation)

Mocks a module with the provided implementation while storing the original for later restoration.

Parameter Type Description
modulePath string Path to the module to mock
mockImplementation () => MockedModule Function that returns the mock implementation

restore(modulePath)

Restores a specific module to its original implementation.

restoreAll()

Restores all mocked modules to their original implementations.

Examples

Basic Module Mocking

import { createModuleMocker } from '@rageltd/bun-test-utils';
import { test, expect, beforeEach, afterAll } from 'bun:test';

const mockModules = createModuleMocker();

describe('UserProfile component', () => {
  beforeEach(async () => {
    await mockModules.mock('@/services/userService', () => ({
      userService: {
        getUser: () => Promise.resolve({
          id: 1,
          name: 'Test User',
          email: 'test@example.com'
        })
      }
    }));
  });

  afterAll(() => {
    mockModules.restoreAll();
  });

  test('renders user information', async () => {
    const { UserProfile } = await import('@/components/UserProfile');
    // Test implementation
  });
});

Mocking Multiple Modules

import { createModuleMocker } from '@rageltd/bun-test-utils';

const mockModules = createModuleMocker();

describe('Dashboard component', () => {
  beforeEach(async () => {
    // Mock authentication service
    await mockModules.mock('@/services/auth', () => ({
      authService: {
        getCurrentUser: () => Promise.resolve({ id: 1, name: 'John' }),
        isAuthenticated: () => true
      }
    }));

    // Mock settings service
    await mockModules.mock('@/services/settings', () => ({
      settingsService: {
        getUserSettings: () => Promise.resolve({
          theme: 'dark',
          language: 'en'
        })
      }
    }));

    // Mock API service
    await mockModules.mock('@/services/api', () => ({
      apiService: {
        fetchDashboardData: () => Promise.resolve({
          stats: { users: 100, sales: 5000 }
        }),
        updateUserPreferences: () => Promise.resolve({ success: true })
      }
    }));
  });

  afterAll(() => {
    mockModules.restoreAll();
  });

  test('renders dashboard with all services', async () => {
    const { Dashboard } = await import('@/components/Dashboard');
    // Test implementation
  });
});

Conditional Mocking

import { createModuleMocker } from '@rageltd/bun-test-utils';

const mockModules = createModuleMocker();

describe('PaymentForm', () => {
  beforeEach(async () => {
    // Mock payment providers based on test environment
    if (process.env.MOCK_STRIPE !== 'false') {
      await mockModules.mock('@stripe/stripe-js', () => ({
        loadStripe: () => Promise.resolve({
          elements: () => ({}),
          createPaymentMethod: () => Promise.resolve({ id: 'pm_test' })
        })
      }));
    }

    // Always mock internal APIs
    await mockModules.mock('@/services/payment', () => ({
      paymentService: {
        processPayment: () => Promise.resolve({
          success: true,
          transactionId: 'tx_123'
        }),
        validateCard: () => Promise.resolve(true)
      }
    }));
  });

  afterAll(() => {
    mockModules.restoreAll();
  });
});

Testing with Real Modules

describe('Integration tests', () => {
  beforeEach(async () => {
    // Mock only external dependencies
    await mockModules.mock('axios', () => ({
      default: {
        get: createMock(() => Promise.resolve({ data: mockApiResponse })),
        post: createMock(() => Promise.resolve({ data: { success: true } }))
      }
    }));

    // Keep internal modules real for integration testing
    // Don't mock @/utils, @/components, etc.
  });

  test('full workflow with real internal modules', async () => {
    // This will use real internal modules but mocked external ones
    const { CompleteWorkflow } = await import('@/workflows/CompleteWorkflow');
    // Test implementation
  });
});

restoreModules

Signature

function restoreModules(modulesMap: Record<string, unknown>): void

Description

A utility function for restoring modules when you've stored their original implementations manually. This is useful for advanced use cases where you need more control over the restoration process.

Parameters

Parameter Type Description
modulesMap Record<string, unknown> Map of module paths to their original implementations

Example

import { restoreModules } from '@rageltd/bun-test-utils';

// Store originals at the top of the test file
const originals = {
  hooks: await import('@/hooks'),
  utils: await import('@/utils'),
  api: await import('@/services/api')
};

describe('Advanced module mocking', () => {
  beforeEach(() => {
    mock.module('@/hooks', () => ({
      useUser: createMockHook('useUser', mockUserData)
    }));

    mock.module('@/utils', () => ({
      formatDate: createMock(() => '2023-01-01'),
      calculateTotal: createMock(() => 100)
    }));
  });

  afterAll(() => {
    restoreModules({
      '@/hooks': originals.hooks,
      '@/utils': originals.utils,
      '@/services/api': originals.api
    });
  });
});

Best Practices

1. Use createModuleMocker

Prefer createModuleMocker over manual module mocking:

// ✅ Recommended approach
const mockModules = createModuleMocker();

beforeEach(async () => {
  await mockModules.mock('@/hooks', () => ({ /* mocks */ }));
});

afterAll(() => {
  mockModules.restoreAll();
});
// ❌ Avoid manual mocking without proper cleanup
beforeEach(() => {
  mock.module('@/hooks', () => ({ /* mocks */ }));
});

afterEach(() => {
  mock.restore(); // This doesn't work properly in Bun
});

2. Mock Before Import

Always mock modules before importing the code under test:

// ❌ Import before mocking
import { UserService } from '@/services/UserService';

beforeEach(async () => {
  await mockModules.mock('@/api/users', () => ({ /* mocks */ }));
});
// ✅ Mock before importing
beforeEach(async () => {
  await mockModules.mock('@/api/users', () => ({ /* mocks */ }));
});

test('user service works', async () => {
  const { UserService } = await import('@/services/UserService');
  // Test implementation
});

3. Organize by Scope

Group related mocks together and use descriptive names:

describe('E-commerce checkout flow', () => {
  beforeEach(async () => {
    // Authentication mocks
    await mockModules.mock('@/auth', () => ({
      useAuth: createMockHook('useAuth', authenticatedUser),
      checkPermissions: createMock(() => true)
    }));

    // Payment mocks
    await mockModules.mock('@/payment', () => ({
      usePayment: createMockHook('usePayment', {
        methods: mockPaymentMethods,
        process: createMock(() => Promise.resolve(successResponse))
      })
    }));

    // Inventory mocks
    await mockModules.mock('@/inventory', () => ({
      checkStock: createMock(() => Promise.resolve(true)),
      reserveItems: createMock(() => Promise.resolve(reservation))
    }));
  });
});

4. Ensure Test Isolation

Use proper cleanup to prevent test pollution:

describe('Component tests', () => {
  const mockModules = createModuleMocker();

  // Clean setup for each test
  beforeEach(async () => {
    await mockModules.mock('@/hooks', () => defaultMocks);
  });

  // Clean teardown for the suite
  afterAll(() => {
    mockModules.restoreAll();
  });

  test('test 1', () => {
    // This test gets fresh mocks
  });

  test('test 2', () => {
    // This test also gets fresh mocks
  });
});

Advanced Patterns

Factory Pattern for Mocks

// Create reusable mock factories
function createUserMocks(userData = {}) {
  return {
    useUser: createMockHook('useUser', {
      id: 1,
      name: 'Default User',
      ...userData
    }),
    useUserPreferences: createMockHook('useUserPreferences', {
      theme: 'light',
      language: 'en'
    })
  };
}

function createApiMocks(responses = {}) {
  return {
    get: createMock(() => Promise.resolve(responses.get || {})),
    post: createMock(() => Promise.resolve(responses.post || {})),
    put: createMock(() => Promise.resolve(responses.put || {})),
    delete: createMock(() => Promise.resolve(responses.delete || {}))
  };
}

// Use in tests
describe('User management', () => {
  beforeEach(async () => {
    await mockModules.mock('@/hooks/user', () =>
      createUserMocks({ name: 'Test User', role: 'admin' })
    );

    await mockModules.mock('@/api', () =>
      createApiMocks({
        get: { users: [{ id: 1, name: 'Test User' }] }
      })
    );
  });
});

Conditional Mocking

// Environment-based mocking
function setupEnvironmentMocks(env = 'test') {
  const configs = {
    test: {
      api: () => createApiMocks(testResponses),
      auth: () => ({ useAuth: createMockHook('useAuth', testUser) })
    },
    development: {
      api: () => createApiMocks(devResponses),
      auth: () => ({ useAuth: createMockHook('useAuth', devUser) })
    },
    staging: {
      // Use real APIs but mock external services
      stripe: () => ({ loadStripe: createMock() }),
      analytics: () => ({ track: createMock() })
    }
  };

  return configs[env] || configs.test;
}

describe('Environment-aware tests', () => {
  beforeEach(async () => {
    const mocks = setupEnvironmentMocks(process.env.NODE_ENV);

    for (const [modulePath, mockFactory] of Object.entries(mocks)) {
      await mockModules.mock(`@/${modulePath}`, mockFactory);
    }
  });
});

Partial Module Mocking

// Mock only specific exports from a module
describe('Partial module mocking', () => {
  beforeEach(async () => {
    // Get the original module
    const originalUtils = await import('@/utils');

    // Mock only specific functions
    await mockModules.mock('@/utils', () => ({
      ...originalUtils,
      // Keep real implementations for most functions
      formatDate: originalUtils.formatDate,
      calculateTotal: originalUtils.calculateTotal,

      // Mock only the problematic ones
      makeApiCall: createMock(() => Promise.resolve(mockData)),
      sendEmail: createMock(() => Promise.resolve(true))
    }));
  });
});

Troubleshooting

Module Not Being Mocked

Ensure modules are mocked before any imports that depend on them:

// ❌ The component is already imported with real dependencies
import { MyComponent } from '@/components/MyComponent';

beforeEach(async () => {
  await mockModules.mock('@/hooks', () => mockHooks);
});
// ✅ Mock first, then import
beforeEach(async () => {
  await mockModules.mock('@/hooks', () => mockHooks);
});

test('component test', async () => {
  const { MyComponent } = await import('@/components/MyComponent');
  // Now MyComponent uses the mocked hooks
});

Mock Pollution Between Tests

Use proper cleanup to prevent mocks from affecting other tests:

// Each test file should have its own mock instance
const mockModules = createModuleMocker();

// Clean up after all tests in this file
afterAll(() => {
  mockModules.restoreAll();
});

// Reset mocks between tests if needed
beforeEach(async () => {
  // Re-establish fresh mocks for each test
  await mockModules.mock('@/hooks', () => freshMocks);
});

TypeScript Errors

Ensure your mocks match the original module's interface:

// Define interfaces for better type safety
interface ApiModule {
  get: (url: string) => Promise<any>;
  post: (url: string, data: any) => Promise<any>;
}

beforeEach(async () => {
  await mockModules.mock('@/api', (): ApiModule => ({
    get: createMock(() => Promise.resolve(mockResponse)),
    post: createMock(() => Promise.resolve(mockResponse))
  }));
});

See Also