Module Mocking
The
module mocking utilities provide a robust solution for mocking entire modules in Bun tests. These utilities work around known issues inbun: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
- Module Mocking Examples - Comprehensive examples and patterns
- Cleanup Utilities - For managing test cleanup
- Working with Bun - Bun-specific testing considerations