Testing Guide
Comprehensive testing strategies and patterns for the TypeScript Project Template.
Testing Philosophy
The template follows a comprehensive testing approach:
- Unit tests for individual functions
- Integration tests for component interactions
- Coverage requirements (80% minimum)
- Async testing patterns for modern JavaScript
Test Structure
File Organization
__tests__/
├── index.test.ts # Main function tests
└── utils.test.ts # Utility function tests
Tests mirror the src/
directory structure for easy navigation.
Running Tests
Basic Commands
bash
# Run all tests
npm test
# Watch mode (auto-rerun on changes)
npm run test:watch
# Generate coverage report
npm run test:coverage
# CI mode (no watch, single run)
npm run test:ci
Coverage Reports
Coverage reports are generated in the coverage/
directory:
- HTML report:
coverage/lcov-report/index.html
- JSON data:
coverage/coverage-final.json
- Text summary: Displayed in terminal
Writing Tests
Basic Test Structure
typescript
import { functionToTest } from '../src/module.js';
describe('functionToTest', () => {
it('should handle normal input', () => {
const result = functionToTest('input');
expect(result).toBe('expected');
});
it('should handle edge cases', () => {
const result = functionToTest('');
expect(result).toBe('default');
});
it('should throw on invalid input', () => {
expect(() => functionToTest(null)).toThrow();
});
});
Async Testing
typescript
import { asyncFunction } from '../src/utils.js';
describe('asyncFunction', () => {
it('should resolve with correct value', async () => {
const result = await asyncFunction('input');
expect(result).toBe('expected');
});
it('should reject on error', async () => {
await expect(asyncFunction('invalid')).rejects.toThrow();
});
});
Mocking Examples
typescript
import { jest } from '@jest/globals';
// Mock console.log
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
afterEach(() => {
mockConsoleLog.mockClear();
});
afterAll(() => {
mockConsoleLog.mockRestore();
});
Test Categories
1. Expected Use Cases
Test normal, expected usage patterns:
typescript
it('should format timestamp correctly', () => {
const timestamp = new Date('2024-01-15T14:30:00.000Z').getTime();
const result = formatTimestamp(timestamp);
expect(result).toBe('2024-01-15 14:30:00');
});
2. Edge Cases
Test boundary conditions and unusual inputs:
typescript
it('should handle zero delay', async () => {
const startTime = Date.now();
await delay(0);
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(50);
});
3. Error Conditions
Test error handling and failure scenarios:
typescript
it('should handle invalid JSON gracefully', () => {
const result = safeJsonParse('invalid json');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('Unexpected token');
}
});
Testing Patterns
Type-Safe Testing
typescript
// Use type guards in tests
if (result.success) {
// TypeScript knows result.data exists
expect(result.data).toEqual({ key: 'value' });
} else {
// TypeScript knows result.error exists
expect(result.error).toBeTruthy();
}
Testing Utilities
typescript
// Use global test utilities from jest.setup.js
const testData = global.testUtils.generateTestData(5);
expect(testData).toHaveLength(5);
await global.testUtils.wait(100);
Configuration Testing
typescript
it('should handle debug configuration', () => {
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
const config = { debug: true };
formatTimestamp(Date.now(), config);
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('Formatted timestamp')
);
mockConsoleLog.mockRestore();
});
Coverage Requirements
Minimum Thresholds
The template enforces 80% coverage across all metrics:
- Branches: 80%
- Functions: 80%
- Lines: 80%
- Statements: 80%
Coverage Configuration
javascript
// jest.config.js
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
}
Improving Coverage
- Identify uncovered code in coverage reports
- Add tests for missing branches and functions
- Test error conditions and edge cases
- Remove dead code if coverage reveals unused code
Best Practices
Test Organization
- Group related tests with
describe
blocks - Use descriptive test names that explain the scenario
- Keep tests focused on single behaviors
- Avoid test interdependencies
Test Data
- Use meaningful test data that represents real usage
- Create test utilities for common data generation
- Avoid magic numbers and strings
- Use constants for expected values
Async Testing
- Always await async operations in tests
- Test both success and failure scenarios
- Use proper timeout values for long operations
- Mock external dependencies to avoid flaky tests
Mocking Guidelines
- Mock external dependencies (APIs, file system, etc.)
- Restore mocks after each test
- Use type-safe mocks when possible
- Verify mock calls when behavior matters
Debugging Tests
Common Issues
bash
# Test timeout issues
npm test -- --testTimeout=10000
# Debug specific test
npm test -- --testNamePattern="specific test name"
# Run tests in band (no parallel)
npm test -- --runInBand
# Verbose output
npm test -- --verbose
VS Code Integration
Configure VS Code for test debugging:
json
{
"type": "node",
"request": "launch",
"name": "Jest Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand"],
"console": "integratedTerminal"
}
Continuous Integration
CI Test Configuration
bash
# Run tests in CI mode
npm run test:ci
# Generate coverage for CI
npm run test:coverage
GitHub Actions Integration
Tests run automatically on:
- Pull requests to main branch
- Pushes to main branch
- Multiple Node.js versions (18, 20)
Next Steps
- Building Guide - Understand the build process
- Quality Gates - Master quality assurance
- API Reference - Explore testable functions