Introduction: Why testing in TypeScript is a must have
TypeScript is a powerful tool that adds static typing to JavaScript. However, even the strictest type system does not guarantee that your code will work correctly at runtime. Bugs, logical errors, incorrect handling of edge cases — all of this remains outside the scope of compilation. This is where testing comes to the rescue.
In this article, we will cover three main levels of testing in TypeScript: unit, integration, and e2e (end-to-end). You will learn how they differ, when to use which, and see real code examples. After reading, you will be able to set up a complete test pipeline for your TypeScript project.
1. Unit testing: checking atomic units of code
Unit tests are the fastest and cheapest level of verification. They isolate a single function, method, or class and test it in isolation from external dependencies (databases, APIs, file system).
1.1 Choosing a tool and setup
The most popular framework for TypeScript is Jest. It has built-in support for TypeScript via ts-jest or @swc/jest.
// Installationnpm install --save-dev jest @types/jest ts-jest
// Initialize configurationnpx ts-jest config:initAfter this, a jest.config.js file with settings will appear in the root directory.
1.2 Example of a unit test for a function
Let's say we have a simple function for calculating a discount:
// discount.tsexport function applyDiscount(price: number, discountPercent: number): number { if (discountPercent < 0 || discountPercent > 100) { throw new Error('Invalid discount percentage'); } return price - (price * discountPercent) / 100;}Let's write a unit test for it:
// discount.test.tsimport { applyDiscount } from './discount';
describe('applyDiscount', () => { it('should correctly calculate a 10% discount', () => { expect(applyDiscount(1000, 10)).toBe(900); });
it('should return the original price with a 0% discount', () => { expect(applyDiscount(500, 0)).toBe(500); });
it('should throw an error for a discount greater than 100%', () => { expect(() => applyDiscount(100, 150)).toThrow('Invalid discount percentage'); });});Run: npx jest. All three tests should pass green.
1.3 Mocks and stubs
If a function accesses an external service (e.g., a database), we replace it with a mock:
// userService.test.tsimport { getUser } from './userService';import { db } from './database';
jest.mock('./database'); // Automatic mock
test('getUser returns a user by ID', async () => { (db.findUser as jest.Mock).mockResolvedValue({ id: 1, name: 'Ivan' }); const user = await getUser(1); expect(user.name).toBe('Ivan');});2. Integration testing: checking module interaction
Integration tests check how different parts of the system work together: controllers, services, repositories, databases. They are slower than unit tests but provide more confidence in the correctness of the connections.
2.1 Testing an API endpoint with Express
For testing HTTP requests, we use the Supertest library together with Jest.
// Installationnpm install --save-dev supertest @types/supertestLet's assume we have an Express application:
// app.tsimport express from 'express';import { createUser } from './controllers/userController';
export const app = express();app.use(express.json());app.post('/api/users', createUser);Integration test:
// app.test.tsimport request from 'supertest';import { app } from './