Cleanup Utilities
The cleanup utilities module provides functions for properly managing test cleanup and isolation. These utilities ensure that tests don't interfere with each other by automatically restoring mocks and cleaning up test state.
Important: Proper test cleanup is critical for reliable test suites. Without cleanup, test pollution can cause flaky tests and hard-to-debug failures.
setupTestCleanup
Signature
function setupTestCleanup(): void
Description
Sets up automatic cleanup for tests by registering an
afterEach hook that calls mock.restore().
This should be called once per test file to ensure all mocks are
properly restored between tests.
Examples
Basic Setup
import { setupTestCleanup } from '@rageltd/bun-test-utils';
import { test, expect } from 'bun:test';
// Call once at the top of your test file
setupTestCleanup();
describe('User service tests', () => {
test('first test', () => {
// Test implementation
// Mocks will be automatically restored after this test
});
test('second test', () => {
// This test starts with a clean state
// No interference from the previous test's mocks
});
});
With Module Mocking
import { setupTestCleanup, createModuleMocker } from '@rageltd/bun-test-utils';
// Setup automatic cleanup
setupTestCleanup();
const mockModules = createModuleMocker();
describe('Component tests', () => {
beforeEach(async () => {
await mockModules.mock('@/services', () => ({
userService: {
getUser: () => Promise.resolve(defaultUser)
}
}));
});
afterAll(() => {
// Clean up module mocks (setupTestCleanup handles function mocks)
mockModules.restoreAll();
});
test('renders user component', () => {
// Test implementation
});
});
withMockCleanup
Signature
function withMockCleanup(testSuiteFn: () => void): void
Description
A higher-order function that wraps a test suite with proper mock cleanup. It creates a module mocker instance and automatically restores all mocks after the test suite completes.
Parameters
| Parameter | Type | Description |
|---|---|---|
testSuiteFn |
() => void |
Function that contains the test suite |
Examples
Wrapping a Test Suite
import { withMockCleanup } from '@rageltd/bun-test-utils';
withMockCleanup(() => {
describe('User management', () => {
test('creates user', () => {
// Test implementation
// All mocks will be automatically cleaned up
});
test('updates user', () => {
// Clean slate for this test
});
});
});
Nested Test Suites
import { withMockCleanup } from '@rageltd/bun-test-utils';
withMockCleanup(() => {
describe('Authentication', () => {
describe('login', () => {
test('successful login', () => {
// Test implementation
});
test('failed login', () => {
// Test implementation
});
});
describe('logout', () => {
test('successful logout', () => {
// Test implementation
});
});
// All mocks from all nested tests will be cleaned up
});
});
Advanced Cleanup Patterns
Custom Cleanup Functions
import { setupTestCleanup } from '@rageltd/bun-test-utils';
import { afterEach } from 'bun:test';
// Setup standard cleanup
setupTestCleanup();
// Add custom cleanup
const customCleanupTasks: (() => void)[] = [];
afterEach(() => {
// Run custom cleanup tasks
customCleanupTasks.forEach(task => task());
customCleanupTasks.length = 0; // Clear the array
});
// Helper to register cleanup tasks
function addCleanupTask(task: () => void) {
customCleanupTasks.push(task);
}
describe('Database tests', () => {
test('creates and cleans up test data', () => {
const testData = createTestData();
// Register cleanup for this specific test
addCleanupTask(() => {
deleteTestData(testData.id);
});
// Test implementation
// testData will be cleaned up automatically
});
});
Conditional Cleanup
import { setupTestCleanup } from '@rageltd/bun-test-utils';
// Only setup cleanup in test environment
if (process.env.NODE_ENV === 'test') {
setupTestCleanup();
}
describe('Environment-aware tests', () => {
test('runs with appropriate cleanup', () => {
// Cleanup behavior depends on environment
});
});
Scoped Cleanup
import { createModuleMocker } from '@rageltd/bun-test-utils';
describe('Feature A tests', () => {
const mockModules = createModuleMocker();
beforeEach(async () => {
await mockModules.mock('@/services/featureA', () => featureAMocks);
});
afterAll(() => {
mockModules.restoreAll();
});
test('feature A works', () => {
// Test implementation
});
});
describe('Feature B tests', () => {
const mockModules = createModuleMocker();
beforeEach(async () => {
await mockModules.mock('@/services/featureB', () => featureBMocks);
});
afterAll(() => {
mockModules.restoreAll();
});
test('feature B works', () => {
// Test implementation
});
});
Best Practices
1. Always Use Cleanup
Every test file should have some form of cleanup:
// ✅ Good - automatic cleanup
import { setupTestCleanup } from '@rageltd/bun-test-utils';
setupTestCleanup();
// ❌ Bad - no cleanup, tests can interfere with each other
describe('Tests without cleanup', () => {
test('test 1', () => {
// Mocks may persist to next test without cleanup
});
});
2. Layer Cleanup Strategies
Use different cleanup strategies for different types of resources:
import { setupTestCleanup, createModuleMocker } from '@rageltd/bun-test-utils';
// Layer 1: Function mocks (automatic)
setupTestCleanup();
// Layer 2: Module mocks (manual)
const mockModules = createModuleMocker();
// Layer 3: External resources (custom)
const cleanupTasks: (() => Promise)[] = [];
beforeEach(() => {
// Setup for each test
});
afterEach(async () => {
// Custom cleanup tasks
await Promise.all(cleanupTasks.map(task => task()));
cleanupTasks.length = 0;
});
afterAll(() => {
// Module cleanup
mockModules.restoreAll();
});
3. Ensure Test Isolation
Each test should start with a clean slate:
describe('Isolated tests', () => {
let service: UserService;
beforeEach(() => {
// Create fresh instance for each test
service = new UserService();
});
test('test 1', () => {
service.addUser(user1);
expect(service.getUsers()).toHaveLength(1);
});
test('test 2', () => {
// Fresh service instance, no users from previous test
expect(service.getUsers()).toHaveLength(0);
});
});
4. Clean Up Async Resources
Don't forget to clean up promises, timers, and other async resources:
describe('Async cleanup', () => {
const activeTimers: NodeJS.Timeout[] = [];
const activePromises: Promise[] = [];
afterEach(() => {
// Clear timers
activeTimers.forEach(clearTimeout);
activeTimers.length = 0;
// Cancel promises if possible
activePromises.length = 0;
});
test('handles timers', async () => {
const timer = setTimeout(() => {
// Timer logic
}, 1000);
activeTimers.push(timer);
// Test implementation
});
});
Common Cleanup Scenarios
React Component Cleanup
import { render, cleanup } from '@testing-library/react';
import { setupTestCleanup } from '@rageltd/bun-test-utils';
setupTestCleanup();
describe('React component tests', () => {
afterEach(() => {
// Clean up rendered components
cleanup();
});
test('renders component', () => {
render( );
// Component will be cleaned up automatically
});
});
DOM Cleanup
import { setupTestCleanup } from '@rageltd/bun-test-utils';
setupTestCleanup();
describe('DOM manipulation tests', () => {
const createdElements: HTMLElement[] = [];
afterEach(() => {
// Remove any elements created during tests
createdElements.forEach(element => {
element.remove();
});
createdElements.length = 0;
});
test('creates DOM elements', () => {
const div = document.createElement('div');
document.body.appendChild(div);
createdElements.push(div);
// Test implementation
// div will be removed after test
});
});
API Cleanup
import { setupTestCleanup } from '@rageltd/bun-test-utils';
setupTestCleanup();
describe('API integration tests', () => {
const createdResources: string[] = [];
afterEach(async () => {
// Clean up any resources created during tests
for (const resourceId of createdResources) {
await deleteResource(resourceId);
}
createdResources.length = 0;
});
test('creates and uses API resource', async () => {
const resource = await createResource({ name: 'test' });
createdResources.push(resource.id);
// Test implementation
// resource will be deleted after test
});
});
Troubleshooting Cleanup
Cleanup Not Running
Ensure cleanup functions are properly registered:
// ❌ Cleanup function not registered
function myCleanup() {
// This won't run automatically
}
// ✅ Properly registered cleanup
afterEach(() => {
myCleanup();
});
Cleanup Order Issues
Be careful about the order of cleanup operations:
// Cleanup runs in reverse order of registration
afterEach(() => {
console.log('Third'); // Runs first
});
afterEach(() => {
console.log('Second'); // Runs second
});
afterEach(() => {
console.log('First'); // Runs last
});
// Output: Third, Second, First
Async Cleanup Issues
Make sure async cleanup operations complete:
// ✅ Proper async cleanup
afterEach(async () => {
await cleanupAsyncResource();
await anotherAsyncCleanup();
});
// ❌ Incorrect - cleanup may not complete
afterEach(() => {
cleanupAsyncResource(); // Promise not awaited
});
Debugging Cleanup Issues
Verbose Cleanup
import { setupTestCleanup } from '@rageltd/bun-test-utils';
// Enable verbose cleanup logging
const originalRestore = mock.restore;
mock.restore = function() {
console.log('Cleaning up mocks...');
return originalRestore.call(this);
};
setupTestCleanup();
// Add logging to custom cleanup
afterEach(() => {
console.log('Running custom cleanup...');
// Custom cleanup code
});
Cleanup Verification
describe('Cleanup verification', () => {
test('verifies cleanup works', () => {
// Create and use mocks
// Test implementation here
});
test('verifies clean state', () => {
// This test should pass if cleanup worked
// No state from previous test should remain
});
});
See Also
- Module Mocking - For module cleanup patterns
- Testing Patterns - Best practices for test organization
- Working with Bun - Bun-specific considerations