Зачем тестировать API
▸Преимущества
Настройка Jest
▸Установка
1npm install -D jest @types/jest ts-jest
▸Конфигурация jest.config.js
1module.exports = {2 preset: 'ts-jest',3 testEnvironment: 'node',4 roots: ['<rootDir>/tests'],5 testMatch: ['**/*.test.ts'],6 collectCoverage: true,7 coverageDirectory: 'coverage',8};
Первый тест
1import { sum } from '../src/utils';23describe('sum', () => {4 it('should add two numbers', () => {5 expect(sum(1, 2)).toBe(3);6 });78 it('should handle negative numbers', () => {9 expect(sum(-1, -2)).toBe(-3);10 });11});
Тестирование Express API с Supertest
▸Установка
1npm install -D supertest
▸Тест эндпоинта
1import request from 'supertest';2import app from '../src/app';34describe('GET /api/users', () => {5 it('should return list of users', async () => {6 const response = await request(app)7 .get('/api/users')8 .expect(200);910 expect(response.body).toBeInstanceOf(Array);11 expect(response.body.length).toBeGreaterThan(0);12 });13});1415describe('POST /api/users', () => {16 it('should create a new user', async () => {17 const response = await request(app)18 .post('/api/users')19 .send({ name: 'Иван', email: 'ivan@example.com' })20 .expect(201);2122 expect(response.body).toHaveProperty('id');23 expect(response.body.name).toBe('Иван');24 });2526 it('should return 400 for invalid data', async () => {27 await request(app)28 .post('/api/users')29 .send({ name: '' })30 .expect(400);31 });32});
Интеграционные тесты
▸Тест с базой данных
1import { PrismaClient } from '@prisma/client';23const prisma = new PrismaClient();45beforeAll(async () => {6 await prisma.$connect();7});89afterAll(async () => {10 await prisma.user.deleteMany();11 await prisma.$disconnect();12});1314describe('User API', () => {15 let userId: number;1617 it('should create user', async () => {18 const res = await request(app)19 .post('/api/users')20 .send({ name: 'Иван', email: 'ivan@test.com' });2122 userId = res.body.id;23 expect(res.status).toBe(201);24 });2526 it('should get user by id', async () => {27 const res = await request(app)28 .get(`/api/users/${userId}`);2930 expect(res.status).toBe(200);31 expect(res.body.name).toBe('Иван');32 });3334 it('should delete user', async () => {35 await request(app)36 .delete(`/api/users/${userId}`)37 .expect(204);38 });39});
Мокирование
▸Мок внешних сервисов
1jest.mock('../src/services/email', () => ({2 sendEmail: jest.fn().mockResolvedValue(true),3}));45import { sendEmail } from '../src/services/email';67test('sends welcome email', async () => {8 await createUser({ name: 'Иван', email: 'ivan@test.com' });9 expect(sendEmail).toHaveBeenCalledWith(10 'ivan@test.com',11 'Добро пожаловать!'12 );13});
▸Мок базы данных
1jest.mock('../src/prisma', () => ({2 prisma: {3 user: {4 findMany: jest.fn().mockResolvedValue([5 { id: 1, name: 'Иван' },6 ]),7 findUnique: jest.fn().mockResolvedValue({ id: 1, name: 'Иван' }),8 },9 },10}));
Тестирование middleware
1describe('Auth middleware', () => {2 it('should return 401 without token', async () => {3 await request(app)4 .get('/api/protected')5 .expect(401);6 });78 it('should return 200 with valid token', async () => {9 const token = generateToken({ userId: 1 });10 await request(app)11 .get('/api/protected')12 .set('Authorization', `Bearer ${token}`)13 .expect(200);14 });15});
Тестирование ошибок
1describe('Error handling', () => {2 it('should return 500 on internal error', async () => {3 jest.spyOn(prisma.user, 'findMany')4 .mockRejectedValue(new Error('DB Error'));56 const res = await request(app)7 .get('/api/users')8 .expect(500);910 expect(res.body.error).toBe('Внутренняя ошибка сервера');11 });12});
Тестирование аутентификации
1describe('Authentication', () => {2 it('should login and return token', async () => {3 const res = await request(app)4 .post('/api/auth/login')5 .send({ email: 'ivan@test.com', password: 'password' })6 .expect(200);78 expect(res.body).toHaveProperty('token');9 });1011 it('should reject invalid credentials', async () => {12 await request(app)13 .post('/api/auth/login')14 .send({ email: 'wrong@test.com', password: 'wrong' })15 .expect(401);16 });17});
Coverage
▸Запуск с покрытием
1npx jest --coverage
▸Конфигурация
1// jest.config.js2module.exports: {3 collectCoverage: true,4 coverageThreshold: {5 global: {6 branches: 80,7 functions: 80,8 lines: 80,9 statements: 80,10 },11 },12};
Best Practices
▸Тестируйте поведение, а не реализацию
Не тестируйте внутренности функции, тестируйте что она возвращает.
▸Изолируйте тесты
Каждый тест должен быть независимым. Не используйте данные из других тестов.
▸Используйте beforeEach для очистки
1beforeEach(async () => {2 await prisma.user.deleteMany();3});
Заключение
Jest + Supertest — стандарт тестирования Node.js API. Пишите unit-тесты для логики, интеграционные тесты для API и используйте мокирование для изоляции. Покрытие кода — метрика качества тестов.