401 lines
11 KiB
TypeScript
401 lines
11 KiB
TypeScript
/**
|
|
* Vitest Unit Test Template
|
|
*
|
|
* This template demonstrates best practices for writing unit tests
|
|
* with Vitest for TypeScript projects.
|
|
*
|
|
* Features:
|
|
* - AAA pattern (Arrange-Act-Assert)
|
|
* - Test isolation
|
|
* - Mocking dependencies
|
|
* - Parametric testing
|
|
* - Error handling tests
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
|
|
// Import the module under test
|
|
import { YourModule } from './YourModule';
|
|
|
|
// Import dependencies (to be mocked)
|
|
import { ExternalDependency } from './ExternalDependency';
|
|
|
|
// ============================================================================
|
|
// MOCKS
|
|
// ============================================================================
|
|
|
|
// Mock external dependencies
|
|
vi.mock('./ExternalDependency', () => ({
|
|
ExternalDependency: vi.fn().mockImplementation(() => ({
|
|
fetchData: vi.fn().mockResolvedValue({ data: 'mocked' }),
|
|
processData: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
// ============================================================================
|
|
// TEST SUITE
|
|
// ============================================================================
|
|
|
|
describe('YourModule', () => {
|
|
// ========================================================================
|
|
// SETUP & TEARDOWN
|
|
// ========================================================================
|
|
|
|
let instance: YourModule;
|
|
let mockDependency: any;
|
|
|
|
beforeEach(() => {
|
|
// ARRANGE: Set up fresh instance for each test
|
|
mockDependency = new ExternalDependency();
|
|
instance = new YourModule(mockDependency);
|
|
});
|
|
|
|
afterEach(() => {
|
|
// TEARDOWN: Clean up mocks
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
// ========================================================================
|
|
// HAPPY PATH TESTS
|
|
// ========================================================================
|
|
|
|
describe('methodName', () => {
|
|
it('should perform expected operation with valid input', () => {
|
|
// ARRANGE
|
|
const input = 'test-input';
|
|
const expectedOutput = 'test-output';
|
|
|
|
// ACT
|
|
const result = instance.methodName(input);
|
|
|
|
// ASSERT
|
|
expect(result).toBe(expectedOutput);
|
|
});
|
|
|
|
it('should call dependency with correct parameters', () => {
|
|
// ARRANGE
|
|
const input = 'test-input';
|
|
|
|
// ACT
|
|
instance.methodName(input);
|
|
|
|
// ASSERT
|
|
expect(mockDependency.processData).toHaveBeenCalledWith(input);
|
|
expect(mockDependency.processData).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// ASYNC TESTS
|
|
// ========================================================================
|
|
|
|
describe('asyncMethod', () => {
|
|
it('should resolve with data on success', async () => {
|
|
// ARRANGE
|
|
const mockData = { id: 1, value: 'test' };
|
|
mockDependency.fetchData.mockResolvedValue(mockData);
|
|
|
|
// ACT
|
|
const result = await instance.asyncMethod();
|
|
|
|
// ASSERT
|
|
expect(result).toEqual(mockData);
|
|
expect(mockDependency.fetchData).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle async errors gracefully', async () => {
|
|
// ARRANGE
|
|
const error = new Error('Network error');
|
|
mockDependency.fetchData.mockRejectedValue(error);
|
|
|
|
// ACT & ASSERT
|
|
await expect(instance.asyncMethod()).rejects.toThrow('Network error');
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// EDGE CASES
|
|
// ========================================================================
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty input', () => {
|
|
// ACT
|
|
const result = instance.methodName('');
|
|
|
|
// ASSERT
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle null input', () => {
|
|
// ACT
|
|
const result = instance.methodName(null as any);
|
|
|
|
// ASSERT
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should handle undefined input', () => {
|
|
// ACT
|
|
const result = instance.methodName(undefined as any);
|
|
|
|
// ASSERT
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should handle very large input', () => {
|
|
// ARRANGE
|
|
const largeInput = 'x'.repeat(1000000);
|
|
|
|
// ACT
|
|
const result = instance.methodName(largeInput);
|
|
|
|
// ASSERT
|
|
expect(result).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// ERROR HANDLING
|
|
// ========================================================================
|
|
|
|
describe('error handling', () => {
|
|
it('should throw error for invalid input', () => {
|
|
// ARRANGE
|
|
const invalidInput = -1;
|
|
|
|
// ACT & ASSERT
|
|
expect(() => instance.methodName(invalidInput)).toThrow(
|
|
'Input must be positive'
|
|
);
|
|
});
|
|
|
|
it('should throw specific error type', () => {
|
|
// ARRANGE
|
|
const invalidInput = 'invalid';
|
|
|
|
// ACT & ASSERT
|
|
expect(() => instance.methodName(invalidInput)).toThrow(ValidationError);
|
|
});
|
|
|
|
it('should include error details', () => {
|
|
// ARRANGE
|
|
const invalidInput = 'invalid';
|
|
|
|
// ACT
|
|
try {
|
|
instance.methodName(invalidInput);
|
|
fail('Expected error to be thrown');
|
|
} catch (error) {
|
|
// ASSERT
|
|
expect(error).toBeInstanceOf(ValidationError);
|
|
expect(error.message).toContain('invalid');
|
|
expect(error.code).toBe('INVALID_INPUT');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// PARAMETRIC TESTS (Table-Driven)
|
|
// ========================================================================
|
|
|
|
describe.each([
|
|
{ input: 1, expected: 2 },
|
|
{ input: 2, expected: 4 },
|
|
{ input: 3, expected: 6 },
|
|
{ input: 4, expected: 8 },
|
|
])('methodName($input)', ({ input, expected }) => {
|
|
it(`should return ${expected}`, () => {
|
|
// ACT
|
|
const result = instance.methodName(input);
|
|
|
|
// ASSERT
|
|
expect(result).toBe(expected);
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// STATE MANAGEMENT TESTS
|
|
// ========================================================================
|
|
|
|
describe('state management', () => {
|
|
it('should update internal state correctly', () => {
|
|
// ARRANGE
|
|
const initialState = instance.getState();
|
|
|
|
// ACT
|
|
instance.updateState('new-value');
|
|
|
|
// ASSERT
|
|
const newState = instance.getState();
|
|
expect(newState).not.toBe(initialState);
|
|
expect(newState).toBe('new-value');
|
|
});
|
|
|
|
it('should emit events on state change', () => {
|
|
// ARRANGE
|
|
const eventHandler = vi.fn();
|
|
instance.on('stateChanged', eventHandler);
|
|
|
|
// ACT
|
|
instance.updateState('new-value');
|
|
|
|
// ASSERT
|
|
expect(eventHandler).toHaveBeenCalledWith('new-value');
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// PERFORMANCE TESTS
|
|
// ========================================================================
|
|
|
|
describe('performance', () => {
|
|
it('should complete within reasonable time', () => {
|
|
// ARRANGE
|
|
const startTime = Date.now();
|
|
|
|
// ACT
|
|
instance.methodName('test');
|
|
|
|
// ASSERT
|
|
const endTime = Date.now();
|
|
const executionTime = endTime - startTime;
|
|
expect(executionTime).toBeLessThan(100); // 100ms threshold
|
|
});
|
|
|
|
it('should handle large datasets efficiently', () => {
|
|
// ARRANGE
|
|
const largeDataset = Array.from({ length: 10000 }, (_, i) => i);
|
|
|
|
// ACT
|
|
const startTime = Date.now();
|
|
const result = instance.processBatch(largeDataset);
|
|
const endTime = Date.now();
|
|
|
|
// ASSERT
|
|
expect(result).toHaveLength(10000);
|
|
expect(endTime - startTime).toBeLessThan(1000); // 1s threshold
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// SNAPSHOT TESTS
|
|
// ========================================================================
|
|
|
|
describe('snapshots', () => {
|
|
it('should match snapshot for complex output', () => {
|
|
// ARRANGE
|
|
const input = { id: 1, name: 'Test', nested: { value: 42 } };
|
|
|
|
// ACT
|
|
const result = instance.transform(input);
|
|
|
|
// ASSERT
|
|
expect(result).toMatchSnapshot();
|
|
});
|
|
|
|
it('should match inline snapshot for simple output', () => {
|
|
// ARRANGE
|
|
const input = 'test';
|
|
|
|
// ACT
|
|
const result = instance.methodName(input);
|
|
|
|
// ASSERT
|
|
expect(result).toMatchInlineSnapshot('"test-output"');
|
|
});
|
|
});
|
|
|
|
// ========================================================================
|
|
// TIMER/DEBOUNCE TESTS
|
|
// ========================================================================
|
|
|
|
describe('timers', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllTimers();
|
|
});
|
|
|
|
it('should debounce function calls', () => {
|
|
// ARRANGE
|
|
const callback = vi.fn();
|
|
const debounced = instance.debounce(callback, 1000);
|
|
|
|
// ACT
|
|
debounced();
|
|
debounced();
|
|
debounced();
|
|
|
|
// ASSERT (not called yet)
|
|
expect(callback).not.toHaveBeenCalled();
|
|
|
|
// Fast-forward time
|
|
vi.advanceTimersByTime(1000);
|
|
|
|
// ASSERT (called once)
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should throttle function calls', () => {
|
|
// ARRANGE
|
|
const callback = vi.fn();
|
|
const throttled = instance.throttle(callback, 1000);
|
|
|
|
// ACT
|
|
throttled(); // Called immediately
|
|
throttled(); // Ignored (within throttle window)
|
|
throttled(); // Ignored (within throttle window)
|
|
|
|
// ASSERT
|
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
|
|
// Fast-forward time
|
|
vi.advanceTimersByTime(1000);
|
|
|
|
// ACT
|
|
throttled(); // Called after throttle window
|
|
|
|
// ASSERT
|
|
expect(callback).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// HELPER TYPES & CLASSES (For Examples Above)
|
|
// ============================================================================
|
|
|
|
class ValidationError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public code: string
|
|
) {
|
|
super(message);
|
|
this.name = 'ValidationError';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// BEST PRACTICES CHECKLIST
|
|
// ============================================================================
|
|
|
|
/*
|
|
✅ AAA Pattern (Arrange-Act-Assert)
|
|
✅ Test Isolation (beforeEach creates fresh instance)
|
|
✅ Descriptive Test Names (should do X when Y)
|
|
✅ One Assertion Per Test (when possible)
|
|
✅ Mock External Dependencies
|
|
✅ Test Happy Path
|
|
✅ Test Edge Cases (null, undefined, empty, large)
|
|
✅ Test Error Handling
|
|
✅ Parametric Tests (test.each)
|
|
✅ Async Testing (async/await, rejects, resolves)
|
|
✅ Timer Testing (useFakeTimers)
|
|
✅ Performance Testing (execution time)
|
|
✅ Snapshot Testing (complex outputs)
|
|
✅ No Shared State Between Tests
|
|
✅ Fast Execution (< 1s per test)
|
|
*/
|