Unit Testing with Vitest
This guide covers our unit testing practices using Vitest, including setup, writing tests, and best practices.
Overview
We use Vitest as our testing framework, which provides a fast and modern testing experience with excellent TypeScript support. Our tests are organized alongside the code they test, following the pattern of *.test.ts
or *.spec.ts
files.
Test Structure
Our tests follow a consistent structure:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
describe('Component or Function Name', () => {
beforeEach(() => {
// Setup code before each test
vi.clearAllMocks();
});
afterEach(() => {
// Cleanup code after each test
});
it('should do something specific', async () => {
// Test implementation
});
});
Key Testing Features
Mocking
We use Vitest's built-in mocking capabilities:
// Mocking modules
vi.mock('./module', () => ({
functionName: vi.fn(),
}));
// Mocking specific functions
vi.spyOn(object, 'method').mockImplementation(() => {
// Custom implementation
});
Testing Async Code
it('should handle async operations', async () => {
const result = await asyncFunction();
expect(result).toBe(expectedValue);
});
Testing Error Cases
it('should throw error on invalid input', async () => {
await expect(asyncFunction(invalidInput))
.rejects
.toThrow(ExpectedError);
});
Best Practices
- Isolation: Each test should be independent and not rely on the state of other tests
- Clear Descriptions: Test descriptions should clearly state what is being tested
- Mock External Dependencies: Use mocks for external services, databases, and APIs
- Test Edge Cases: Include tests for error conditions and edge cases
- Use TypeScript: Leverage TypeScript for better type safety in tests
Running Tests
Tests can be run using the following commands:
# Run all tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Run tests for a specific file
pnpm test path/to/test/file.test.ts
Example Test Files
API Handler Test Example
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { secretCache } from '../utils/secretCache';
import { connectDB } from '@b4m/database';
import jwt from 'jsonwebtoken';
import { UnauthorizedError } from '../utils/errors';
describe('Secret caching in API handlers', () => {
const mockHandler = vi.fn();
const mockReq = { headers: {} };
const mockRes = { status: vi.fn().mockReturnThis(), json: vi.fn() };
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch secrets and connect to database', async () => {
// Test implementation
});
});
Utility Test Example
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { secretCache, SecretCacheManager } from './secretCache';
describe('SecretCacheManager', () => {
beforeEach(() => {
secretCache.clearCache();
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should return the same instance on multiple calls', () => {
const instance1 = SecretCacheManager.getInstance();
const instance2 = SecretCacheManager.getInstance();
expect(instance1).toBe(instance2);
});
});
Common Testing Patterns
- Setup and Teardown: Use
beforeEach
andafterEach
for consistent test environment - Mocking Dependencies: Mock external services and dependencies
- Assertions: Use Vitest's assertion library for clear and readable tests
- Async Testing: Properly handle asynchronous code with async/await
- Error Testing: Test both success and error cases
Tips and Tricks
- Use
vi.useFakeTimers()
for testing time-dependent code - Leverage TypeScript's type system to catch errors early
- Keep tests focused and test one thing at a time
- Use descriptive test names that explain the expected behavior
- Mock external services to keep tests fast and reliable