Basic Testing

Learn the fundamentals of testing with @rageltd/bun-test-utils. This guide covers the core patterns and utilities for effective testing in Bun projects.

Prerequisites: This guide assumes familiarity with bun:test and basic testing concepts.

Test Setup

Start every test file with proper setup to ensure clean test isolation:

import { describe, it, expect, beforeEach, afterEach, afterAll } from 'bun:test';
import {
  setupTestCleanup,
  createModuleMocker,
  clearMockRegistry
} from '@rageltd/bun-test-utils';

// Setup automatic cleanup (call once per test file)
setupTestCleanup();

const mockModules = createModuleMocker();

describe('My Test Suite', () => {
  beforeEach(() => {
    // Setup test-specific mocks here
  });

  afterEach(() => {
    // Additional cleanup if needed
    clearMockRegistry();
  });

  afterAll(() => {
    // Restore all module mocks
    mockModules.restoreAll();
  });

  // Your tests here
});

Module Mocking

Mock entire modules to isolate units under test:

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

const mockModules = createModuleMocker();

describe('Service Tests', () => {
  beforeEach(async () => {
    // Mock external dependencies
    await mockModules.mock('@/services/api', () => ({
      apiClient: {
        get: () => Promise.resolve({ data: 'mocked data' }),
        post: () => Promise.resolve({ success: true }),
        delete: () => Promise.resolve({ deleted: true })
      }
    }));

    // Mock utility modules
    await mockModules.mock('@/utils/logger', () => ({
      logger: {
        info: () => {},
        error: () => {},
        warn: () => {}
      }
    }));
  });

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

  it('should handle API calls with mocked services', async () => {
    // Test code that uses the mocked modules
    // The mocks are automatically available
    expect(true).toBe(true); // Your actual test here
  });
});

Cleanup Patterns

Ensure proper test isolation with cleanup utilities:

import {
  setupTestCleanup,
  withMockCleanup,
  clearMockRegistry
} from '@rageltd/bun-test-utils';

// Pattern 1: Manual cleanup setup
describe('Manual Cleanup Example', () => {
  setupTestCleanup(); // Sets up automatic mock.restore()

  afterEach(() => {
    clearMockRegistry(); // Clear custom registry
  });

  it('should clean up properly', () => {
    // Test code here
  });
});

// Pattern 2: Automatic cleanup wrapper
withMockCleanup(() => {
  describe('Automatic Cleanup Example', () => {
    // Cleanup is handled automatically

    it('should handle cleanup behind the scenes', () => {
      // Test code here - cleanup happens automatically
    });
  });
});

Module Restoration

Handle complex module mocking scenarios with proper restoration:

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

// Pattern 1: Using createModuleMocker (recommended)
const mockModules = createModuleMocker();

describe('Module Mocker Pattern', () => {
  beforeEach(async () => {
    await mockModules.mock('@/config', () => ({
      config: { apiUrl: 'http://test.api', timeout: 1000 }
    }));
  });

  afterAll(() => {
    mockModules.restoreAll(); // Restores all mocked modules
  });
});

// Pattern 2: Manual restoration (advanced)
describe('Manual Restoration Pattern', () => {
  // Store originals at the top of test file
  const originals = {
    config: import('@/config'),
    utils: import('@/utils')
  };

  beforeEach(async () => {
    // Apply mocks
    // ... mocking code
  });

  afterAll(async () => {
    // Restore manually
    restoreModules({
      '@/config': await originals.config,
      '@/utils': await originals.utils
    });
  });
});

Testing Best Practices

✅ Do's

  • Always call setupTestCleanup() at the top of test files
  • Use createModuleMocker() for consistent module mocking
  • Call restoreAll() in afterAll hooks
  • Mock only what you need to isolate the unit under test
  • Use descriptive test names that explain the expected behavior
  • Group related tests with describe blocks

❌ Don'ts

  • Don't forget to restore mocks after tests
  • Don't mock implementation details you don't need
  • Don't share mock state between tests
  • Don't rely on test execution order
  • Don't mock everything - test real behavior when possible

Common Testing Patterns

Testing Error Conditions

describe('Error Handling', () => {
  beforeEach(async () => {
    // Mock a service that can throw errors
    await mockModules.mock('@/services/api', () => ({
      apiClient: {
        get: () => Promise.reject(new Error('Network error')),
        post: () => { throw new Error('Validation error'); }
      }
    }));
  });

  it('should handle network errors gracefully', async () => {
    // Test error handling behavior
    await expect(async () => {
      // Code that uses the mocked failing service
    }).toThrow('Network error');
  });

  it('should handle validation errors', () => {
    expect(() => {
      // Code that triggers validation error
    }).toThrow('Validation error');
  });
});

Testing Different Scenarios

describe('Feature Tests', () => {
  it('should handle success case', async () => {
    await mockModules.mock('@/services/user', () => ({
      userService: {
        getUser: () => Promise.resolve({ id: 1, name: 'John Doe' })
      }
    }));

    // Test successful user retrieval
  });

  it('should handle user not found', async () => {
    await mockModules.mock('@/services/user', () => ({
      userService: {
        getUser: () => Promise.resolve(null)
      }
    }));

    // Test null user handling
  });

  it('should handle service unavailable', async () => {
    await mockModules.mock('@/services/user', () => ({
      userService: {
        getUser: () => Promise.reject(new Error('Service unavailable'))
      }
    }));

    // Test service error handling
  });
});

Summary

Key takeaways for effective testing with @rageltd/bun-test-utils:

✅ Always set up proper cleanup with setupTestCleanup()
  • ✅ Use createModuleMocker() for consistent module mocking
  • ✅ Restore all mocks in afterAll hooks
  • ✅ Test both success and error cases
  • ✅ Keep tests isolated and independent
  • ✅ Use clear, descriptive test names