Advanced Testing in TypeScript: from Unit to E2E
Testing is not just a code sanity check, but the foundation of professional TypeScript development. At an advanced level, we move from simple asserts to building isolated, fast, and reliable test pyramids. In this article, we will break down practical approaches to unit, integration, and e2e testing using the best tools in the TypeScript ecosystem.
1. Advanced Unit Testing with Jest and TypeScript
Unit tests check isolated modules. In TypeScript, the key challenge is working with dependencies, types, and asynchronicity.
1.1. Mocks and Typing with jest.mocked
Use jest.mocked for full typing of mocks. This guarantees you won't call a non-existent method.
import { fetchUserData } from './api';import { processUser } from './userProcessor';
jest.mock('./api');
const mockedFetchUserData = jest.mocked(fetchUserData);
describe('processUser', () => { it('should process user data', async () => { const mockUser = { id: 1, name: 'Alice', email: 'alice@test.com' }; mockedFetchUserData.mockResolvedValue(mockUser);
const result = await processUser(1);
expect(result).toEqual({ displayName: 'Alice', contact: 'alice@test.com' }); expect(mockedFetchUserData).toHaveBeenCalledWith(1); });
it('should throw an error when email is missing', async () => { mockedFetchUserData.mockResolvedValue({ id: 2, name: 'Bob', email: null }); await expect(processUser(2)).rejects.toThrow('Email is required'); });});1.2. Testing Complex Types and Generics
For functions with generics, create helper test types to avoid duplication.
// Codefunction getFirstElement<T>(arr: T[]): T | undefined { return arr[0];}
// Testtype TestArray = number[];const array: TestArray = [10, 20, 30];
describe('getFirstElement', () => { it('should return the first number element', () => { const result = getFirstElement(array); expect(result).toBe(10); // TypeScript automatically infers the number type });
it('should return undefined for an empty array', () => { const result = getFirstElement([]); expect(result).toBeUndefined(); });});2. Integration Testing: Database and API
Integration tests check the interaction of modules: controllers, services, and the database. For TypeScript applications on Node.js, the combination of Jest + Supertest + Testcontainers is ideal.
2.1. Setting up a Test Database with Testcontainers
Testcontainers spins up PostgreSQL in Docker, guaranteeing a clean environment for each test.
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';import { Client } from 'pg';
let container: StartedPostgreSqlContainer;let client: Client;
beforeAll(async () => { container = await new PostgreSqlContainer().start(); client = new Client({ connectionString: container.getConnectionUri() }); await client.connect(); await client.query(` CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE NOT NULL ); `);}, 30000); // 30 second timeout for downloading the image
afterAll(async () => { await client.end(); await container.stop();});
describe('User Repository', () => { it('should create and return a user', async () => { const res = await client.query( `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *`, ['Test', 'test@example.com'] ); expect(res.rows[0].name).toBe('Test'); });});2.2. Testing REST API with Supertest
We start the Express application and test