Skip to main content

Testing

Write type-safe, reliable tests for your TypeScript applications using modern testing frameworks that leverage TypeScript's type system.

What's Covered

This section covers testing frameworks, methodologies, and best practices for testing TypeScript code, including how to leverage static typing for better test quality and maintainability.

Why Testing TypeScript is Different

Type Safety Advantages

  • Catch type errors at compile time, not test time
  • Autocomplete and IntelliSense in tests
  • Refactor with confidence
  • Self-documenting test expectations

Testing Challenges

  • Configure test runners for TypeScript
  • Handle type definitions for mocks
  • Test type guards and assertions
  • Balance type safety with test flexibility

Testing TypeScript Code

Type-Safe Test Setup

TypeScript provides additional safety in your tests:

// Types ensure correct test data
interface User {
id: number;
name: string;
email: string;
}

// Test with type safety
test('creates user', () => {
const user: User = {
id: 1,
name: 'Test User',
email: 'test@example.com'
};

const result = createUser(user);
expect(result.success).toBe(true);
});

Mocking with Types

TypeScript ensures mocks match the real interfaces:

// Define interface
interface UserService {
getUser(id: number): Promise<User>;
saveUser(user: User): Promise<void>;
}

// Type-safe mock
const mockUserService: jest.Mocked<UserService> = {
getUser: jest.fn(),
saveUser: jest.fn()
};

// TypeScript ensures we mock correctly
mockUserService.getUser.mockResolvedValue({
id: 1,
name: 'Test',
email: 'test@example.com'
});

Testing Frameworks for TypeScript

Mocha + Chai

Flexible testing framework with BDD/TDD assertion styles.

Key Features:

  • Highly configurable test framework
  • Multiple assertion styles (BDD and TDD)
  • Browser and Node.js support
  • Rich plugin ecosystem
  • First-class TypeScript support

Use Cases:

  • Projects requiring custom test configurations
  • Teams preferring BDD-style assertions
  • Browser and Node.js testing

Read Mocha + Chai Guide →

Cypress

Next-generation end-to-end testing framework.

Key Features:

  • First-class TypeScript support
  • Real browser testing with time travel debugging
  • Automatic waiting and retries
  • Network stubbing and API testing
  • Component testing support

Use Cases:

  • End-to-end web application testing
  • Integration testing of UI components
  • API testing
  • Visual regression testing

Read Cypress Guide →

Jest with ts-jest

Industry-standard testing framework with first-class TypeScript support.

Key Features:

  • Zero configuration for most projects
  • All-in-one: test runner, assertions, mocking, coverage
  • Type-safe mocks with jest.Mocked<T>
  • Snapshot testing
  • Extensive ecosystem and community

Use Cases:

  • React and Vue applications
  • Node.js applications
  • Any TypeScript/JavaScript project
  • Unit and integration testing

Read Jest Guide →

Vitest

Blazing fast unit test framework powered by Vite.

Key Features:

  • Extremely fast (10-100x faster than Jest)
  • Native TypeScript and ESM support
  • Jest-compatible API (easy migration)
  • Built-in component testing
  • Hot module replacement for tests

Use Cases:

  • Modern TypeScript/JavaScript projects
  • Vite-based applications
  • Projects using ES modules
  • Migration from Jest

Read Vitest Guide →

Testing Library

Type-safe DOM testing for TypeScript applications.

Key Features:

  • TypeScript type definitions included
  • Framework-specific libraries (React, Vue, etc.)
  • User-centric testing approach
  • Encourages accessibility best practices
  • Works with Jest or Vitest

Testing TypeScript-Specific Features

Testing Type Guards

// Type guard function
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}

// Test the type guard
test('isUser validates user objects', () => {
const validUser = { id: 1, name: 'Test', email: 'test@example.com' };
const invalidUser = { id: 1, name: 'Test' }; // missing email

expect(isUser(validUser)).toBe(true);
expect(isUser(invalidUser)).toBe(false);
expect(isUser(null)).toBe(false);
expect(isUser('string')).toBe(false);
});

Testing Generic Functions

// Generic function
function toArray<T>(value: T | T[]): T[] {
return Array.isArray(value) ? value : [value];
}

// Test with different types
test('toArray handles various types', () => {
expect(toArray(5)).toEqual([5]);
expect(toArray([1, 2, 3])).toEqual([1, 2, 3]);
expect(toArray('hello')).toEqual(['hello']);
expect(toArray(['a', 'b'])).toEqual(['a', 'b']);
});

Testing Enums

enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Completed = 'COMPLETED'
}

function getStatusColor(status: Status): string {
switch (status) {
case Status.Pending: return 'yellow';
case Status.Active: return 'green';
case Status.Completed: return 'blue';
}
}

test('getStatusColor returns correct colors', () => {
expect(getStatusColor(Status.Pending)).toBe('yellow');
expect(getStatusColor(Status.Active)).toBe('green');
expect(getStatusColor(Status.Completed)).toBe('blue');
});

Testing Classes

class UserRepository {
private users: Map<number, User> = new Map();

add(user: User): void {
this.users.set(user.id, user);
}

get(id: number): User | undefined {
return this.users.get(id);
}

getAll(): User[] {
return Array.from(this.users.values());
}
}

describe('UserRepository', () => {
let repository: UserRepository;

beforeEach(() => {
repository = new UserRepository();
});

test('adds and retrieves users', () => {
const user: User = { id: 1, name: 'Test', email: 'test@example.com' };

repository.add(user);
const retrieved = repository.get(1);

expect(retrieved).toEqual(user);
});

test('getAll returns all users', () => {
const user1: User = { id: 1, name: 'User 1', email: 'user1@example.com' };
const user2: User = { id: 2, name: 'User 2', email: 'user2@example.com' };

repository.add(user1);
repository.add(user2);

expect(repository.getAll()).toHaveLength(2);
});
});

Best Practices

Use Type Definitions

Always define types for test data:

// Good - type-safe
const testUser: User = {
id: 1,
name: 'Test',
email: 'test@example.com'
};

// Bad - no type safety
const testUser = {
id: 1,
name: 'Test',
email: 'test@example.com'
};

Mock External Dependencies

Use typed mocks:

// Create type-safe mock
const mockApiClient: jest.Mocked<ApiClient> = {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
};

// Mock implementation with types
mockApiClient.get.mockResolvedValue<User>({
id: 1,
name: 'Test',
email: 'test@example.com'
});

Test Error Cases

TypeScript helps catch error scenarios:

function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}

test('divide throws on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});

test('divide returns correct result', () => {
expect(divide(10, 2)).toBe(5);
});

Use Test Utilities

Create type-safe test helpers:

// Test factory function
function createTestUser(overrides?: Partial<User>): User {
return {
id: 1,
name: 'Test User',
email: 'test@example.com',
...overrides
};
}

// Use in tests
test('user with custom email', () => {
const user = createTestUser({ email: 'custom@example.com' });
expect(user.email).toBe('custom@example.com');
});

Configuration Tips

tsconfig.json for Tests

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["jest", "node"]
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"]
}

Separate Test Config

// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
},
"include": ["src/**/*", "**/*.test.ts", "**/*.spec.ts"]
}

Testing Philosophy

Effective TypeScript tests:

  • Leverage Types - Use TypeScript's type system to catch errors
  • Type-Safe Mocks - Ensure mocks match real implementations
  • Test Types - Validate type guards and generic functions
  • Fast - Run quickly with proper configuration
  • Maintainable - Easy to update when types change

Getting Started

Quick Setup with Jest

# Install dependencies
npm install --save-dev jest ts-jest @types/jest typescript

# Initialize ts-jest
npx ts-jest config:init

# Add test script
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}

Your First TypeScript Test

// math.ts
export function add(a: number, b: number): number {
return a + b;
}

// math.test.ts
import { add } from './math';

test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});

// TypeScript will catch this error at compile time
// test('type error', () => {
// expect(add('2', '3')).toBe(5); // Error: Argument of type 'string' is not assignable
// });

Next Steps

After mastering TypeScript testing, explore:

  • Advanced Mocking - Mock complex types and interfaces
  • E2E Testing - Playwright with TypeScript
  • Component Testing - Test React/Vue components with types
  • API Testing - Type-safe API integration tests
  • Test-Driven Development - Write types and tests first
  • Property-Based Testing - Fast-check for TypeScript