Testing in TypeScript
/ 7 min read
Setting Up Testing Environment
1. Jest Configuration
module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], transform: { '^.+\\.tsx?$': 'ts-jest' }, testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']};2. TypeScript Configuration
{ "compilerOptions": { "target": "es5", "module": "commonjs", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "types": ["jest", "node"] }, "include": ["src/**/*"], "exclude": ["node_modules"]}Unit Testing
1. Basic Tests
export class Calculator { add(a: number, b: number): number { return a + b; }
subtract(a: number, b: number): number { return a - b; }}
// calculator.test.tsimport { Calculator } from './calculator';
describe('Calculator', () => { let calculator: Calculator;
beforeEach(() => { calculator = new Calculator(); });
test('adds two numbers correctly', () => { expect(calculator.add(2, 3)).toBe(5); });
test('subtracts two numbers correctly', () => { expect(calculator.subtract(5, 3)).toBe(2); });});2. Testing Async Code
export class UserService { async getUser(id: number): Promise<User> { const response = await fetch(`/api/users/${id}`); return response.json(); }}
// user-service.test.tsdescribe('UserService', () => { let service: UserService;
beforeEach(() => { service = new UserService(); });
test('fetches user by id', async () => { const mockUser = { id: 1, name: 'John' }; global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve(mockUser) });
const user = await service.getUser(1); expect(user).toEqual(mockUser); });
test('handles errors', async () => { global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
await expect(service.getUser(1)).rejects.toThrow('Network error'); });});3. Mocking
export class Database { async query(sql: string): Promise<any[]> { // Real database implementation return []; }}
// user-repository.tsexport class UserRepository { constructor(private db: Database) {}
async findById(id: number): Promise<User | null> { const results = await this.db.query(`SELECT * FROM users WHERE id = ${id}`); return results[0] || null; }}
// user-repository.test.tsjest.mock('./database');
describe('UserRepository', () => { let repository: UserRepository; let mockDatabase: jest.Mocked<Database>;
beforeEach(() => { mockDatabase = new Database() as jest.Mocked<Database>; repository = new UserRepository(mockDatabase); });
test('finds user by id', async () => { const mockUser = { id: 1, name: 'John' }; mockDatabase.query.mockResolvedValue([mockUser]);
const user = await repository.findById(1); expect(user).toEqual(mockUser); });});Integration Testing
1. API Testing
import request from 'supertest';import { app } from './app';
describe('User API', () => { test('GET /api/users returns users', async () => { const response = await request(app) .get('/api/users') .expect('Content-Type', /json/) .expect(200);
expect(response.body).toBeInstanceOf(Array); });
test('POST /api/users creates new user', async () => { const newUser = { name: 'John', email: 'john@example.com' };
const response = await request(app) .post('/api/users') .send(newUser) .expect('Content-Type', /json/) .expect(201);
expect(response.body).toMatchObject(newUser); });});2. Database Integration
import { Database } from './database';import { UserRepository } from './user-repository';
describe('UserRepository Integration', () => { let database: Database; let repository: UserRepository;
beforeAll(async () => { database = await Database.connect({ host: 'localhost', database: 'test_db' }); repository = new UserRepository(database); });
afterAll(async () => { await database.disconnect(); });
beforeEach(async () => { await database.query('TRUNCATE TABLE users'); });
test('creates and retrieves user', async () => { const user = await repository.create({ name: 'John', email: 'john@example.com' });
const retrieved = await repository.findById(user.id); expect(retrieved).toMatchObject(user); });});End-to-End Testing with Cypress
1. Basic E2E Test
describe('Login Page', () => { beforeEach(() => { cy.visit('/login'); });
it('should login successfully', () => { cy.get('[data-testid=email]').type('user@example.com'); cy.get('[data-testid=password]').type('password123'); cy.get('[data-testid=submit]').click();
cy.url().should('include', '/dashboard'); cy.get('[data-testid=welcome]').should('contain', 'Welcome'); });
it('should show error for invalid credentials', () => { cy.get('[data-testid=email]').type('invalid@example.com'); cy.get('[data-testid=password]').type('wrongpassword'); cy.get('[data-testid=submit]').click();
cy.get('[data-testid=error]').should('be.visible'); });});2. Custom Commands
declare namespace Cypress { interface Chainable { login(email: string, password: string): void; }}
Cypress.Commands.add('login', (email: string, password: string) => { cy.get('[data-testid=email]').type(email); cy.get('[data-testid=password]').type(password); cy.get('[data-testid=submit]').click();});
// Usage in testdescribe('Protected Pages', () => { beforeEach(() => { cy.login('user@example.com', 'password123'); });
it('accesses protected page', () => { cy.visit('/protected'); cy.get('[data-testid=content]').should('be.visible'); });});Component Testing
1. React Components
interface ButtonProps { label: string; onClick: () => void; disabled?: boolean;}
const Button: React.FC<ButtonProps> = ({ label, onClick, disabled }) => ( <button onClick={onClick} disabled={disabled}> {label} </button>);
// button.test.tsximport { render, fireEvent } from '@testing-library/react';
describe('Button', () => { test('renders with label', () => { const { getByText } = render( <Button label="Click me" onClick={() => {}} /> ); expect(getByText('Click me')).toBeInTheDocument(); });
test('handles click events', () => { const handleClick = jest.fn(); const { getByText } = render( <Button label="Click me" onClick={handleClick} /> );
fireEvent.click(getByText('Click me')); expect(handleClick).toHaveBeenCalled(); });
test('can be disabled', () => { const { getByText } = render( <Button label="Click me" onClick={() => {}} disabled /> ); expect(getByText('Click me')).toBeDisabled(); });});2. Vue Components
<template> <div> <span data-testid="count">{{ count }}</span> <button @click="increment">Increment</button> </div></template>
<script lang="ts">import { defineComponent, ref } from 'vue';
export default defineComponent({ name: 'Counter', setup() { const count = ref(0); const increment = () => count.value++;
return { count, increment }; }});</script>
// counter.test.tsimport { mount } from '@vue/test-utils';import Counter from './Counter.vue';
describe('Counter', () => { test('renders initial count', () => { const wrapper = mount(Counter); expect(wrapper.find('[data-testid="count"]').text()).toBe('0'); });
test('increments count when button is clicked', async () => { const wrapper = mount(Counter); await wrapper.find('button').trigger('click'); expect(wrapper.find('[data-testid="count"]').text()).toBe('1'); });});Testing Utilities and Helpers
1. Test Factories
import { faker } from '@faker-js/faker';
export const createUser = (overrides = {}) => ({ id: faker.datatype.uuid(), name: faker.name.fullName(), email: faker.internet.email(), ...overrides});
export const createProduct = (overrides = {}) => ({ id: faker.datatype.uuid(), name: faker.commerce.productName(), price: faker.commerce.price(), ...overrides});
// Usage in testsdescribe('Shopping Cart', () => { test('calculates total correctly', () => { const products = [ createProduct({ price: '10.00' }), createProduct({ price: '20.00' }) ]; const cart = new ShoppingCart(products); expect(cart.getTotal()).toBe(30.00); });});2. Custom Matchers
expect.extend({ toBeWithinRange(received: number, floor: number, ceiling: number) { const pass = received >= floor && received <= ceiling; if (pass) { return { message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`, pass: true }; } else { return { message: () => `expected ${received} to be within range ${floor} - ${ceiling}`, pass: false }; } }});
declare global { namespace jest { interface Matchers<R> { toBeWithinRange(floor: number, ceiling: number): R; } }}
// Usagetest('numeric ranges', () => { expect(100).toBeWithinRange(90, 110);});Best Practices
- Write tests first (TDD)
- Keep tests focused and isolated
- Use meaningful test descriptions
- Follow the Arrange-Act-Assert pattern
- Don’t test implementation details
- Maintain test data separately
- Use appropriate assertions
- Clean up after tests
Common Testing Patterns
1. Repository Pattern Testing
interface Repository<T> { find(id: string): Promise<T>; create(item: T): Promise<T>; update(id: string, item: T): Promise<T>; delete(id: string): Promise<void>;}
class InMemoryRepository<T> implements Repository<T> { private items: Map<string, T> = new Map();
async find(id: string): Promise<T> { const item = this.items.get(id); if (!item) throw new Error('Not found'); return item; }
// Implement other methods...}
// Testingdescribe('InMemoryRepository', () => { let repository: InMemoryRepository<User>;
beforeEach(() => { repository = new InMemoryRepository<User>(); });
test('creates and finds item', async () => { const user = await repository.create({ id: '1', name: 'John' }); const found = await repository.find('1'); expect(found).toEqual(user); });});2. Service Layer Testing
class UserService { constructor(private repository: Repository<User>) {}
async createUser(data: CreateUserDTO): Promise<User> { // Validate data // Hash password // Create user return this.repository.create(data); }}
// Testingdescribe('UserService', () => { let service: UserService; let mockRepository: jest.Mocked<Repository<User>>;
beforeEach(() => { mockRepository = { create: jest.fn(), find: jest.fn(), update: jest.fn(), delete: jest.fn() }; service = new UserService(mockRepository); });
test('creates user with hashed password', async () => { const userData = { name: 'John', password: 'secret' }; await service.createUser(userData); expect(mockRepository.create).toHaveBeenCalledWith( expect.objectContaining({ name: 'John', password: expect.not.stringContaining('secret') }) ); });});Conclusion
Testing TypeScript applications requires a good understanding of both testing principles and TypeScript’s type system. Proper testing ensures code quality and helps catch bugs early in development.
Series Navigation
- Previous: TypeScript with React/Vue/Angular
- Next: TypeScript Design Patterns