Testing in TypeScript: from unit to e2e — a complete guide
\nModern web development is unthinkable without quality testing. TypeScript, as a superset of JavaScript, provides powerful typing tools but does not solve the problem of logical errors. In this article, we will cover three levels of testing: unit, integration, and e2e (end-to-end). You will learn how to set up the environment, write effective tests in TypeScript, and integrate them into a CI/CD pipeline.
\n\n1. Unit testing: the foundation of the testing pyramid
\nUnit tests check isolated modules (functions, classes, methods) without external dependencies. For TypeScript, the best choice is Jest — it supports TypeScript out of the box and provides a rich API for mocks and assertions.
\n\nSetting up Jest for TypeScript
\nnpm install --save-dev jest @types/jest ts-jest\nnpx ts-jest config:init\nThe jest.config.ts file should contain:
export default {\n preset: 'ts-jest',\n testEnvironment: 'node',\n roots: ['<rootDir>/src'],\n testMatch: ['**/__tests__/**/*.test.ts']\n};\n\nExample of a unit test for a function
\nSuppose we have a discount calculation function:
\n// src/utils/discount.ts\nexport function calculateDiscount(price: number, percent: number): number {\n if (percent < 0 || percent > 100) throw new Error('Invalid percent');\n return price - (price * percent) / 100;\n}\nTest for it:
\n// src/__tests__/discount.test.ts\nimport { calculateDiscount } from '../utils/discount';\n\ndescribe('calculateDiscount', () => {\n it('should return 80 for price 100 and discount 20%', () => {\n expect(calculateDiscount(100, 20)).toBe(80);\n });\n\n it('should throw an error for an invalid percent', () => {\n expect(() => calculateDiscount(100, 150)).toThrow('Invalid percent');\n });\n\n it('should return 0 for a 100% discount', () => {\n expect(calculateDiscount(50, 100)).toBe(0);\n });\n});\n\nWorking with mocks
\nUse mocks to isolate tests. Example with a service that depends on a database:
\n// src/services/user.service.ts\nimport { db } from '../database';\n\nexport class UserService {\n async getUser(id: number) {\n const user = await db.findUser(id);\n if (!user) throw new Error('User not found');\n return user;\n }\n}\n// src/__tests__/user.service.test.ts\nimport { UserService } from '../services/user.service';\nimport { db } from '../database';\n\njest.mock('../database');\n\ndescribe('UserService', () => {\n it('should return a user', async () => {\n const mockUser = { id: 1, name: 'Alice' };\n (db.findUser as jest.Mock).mockResolvedValue(mockUser);\n\n const service = new UserService();\n const result = await service.getUser(1);\n expect(result).toEqual(mockUser);\n });\n});\n\n2. Integration testing: checking component interaction
\nIntegration tests check how modules work together: API routes, databases, external services. For TypeScript applications on Node.js, Supertest is often used together with Jest.
\n\nSetting up an integration test for Express/NestJS
\nnpm install --save-dev supertest @types/supertest\nExample test for a REST API (NestJS):
\n// src/__tests__/app.integration.test.ts\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication } from '@nestjs/common';\nimport * as request from 'supertest';\nimport { AppModule } from '../app.module';\n\ndescribe('AppController (e2e)', () => {\n let app: INestApplication;\n\n beforeAll(async () => {\n const moduleFixture: TestingModule = await Test.createTestingModule({\n imports: [AppModule],\n }).compile();\n\n a