Skip to content

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

  1. Identify uncovered code in coverage reports
  2. Add tests for missing branches and functions
  3. Test error conditions and edge cases
  4. 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

Released under the MIT License.