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