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