Testing Patterns - Node.js/TypeScript Backend
Testing Pyramid
/\
/E2E\ <- Poucos, fluxos críticos de API
/------\
/ INT \ <- Alguns, endpoints completos
/----------\
/ UNIT \ <- Muitos, lógica de negócio
/--------------\
Proporção recomendada: 70% Unit, 20% Integration, 10% E2E
Setup: Jest + Supertest
Instalação
bash
1npm install -D jest @types/jest ts-jest supertest @types/supertest
2npm install -D mongodb-memory-server # Para testes com MongoDB
Configuração
typescript
1// jest.config.ts
2export default {
3 preset: 'ts-jest',
4 testEnvironment: 'node',
5 setupFilesAfterEnv: ['<rootDir>/__test__/globalSetup.ts'],
6 testMatch: ['**/__test__/**/*.test.ts'],
7 collectCoverageFrom: [
8 'src/**/*.ts',
9 '!src/**/*.d.ts',
10 '!src/server/main.ts',
11 ],
12 coverageThreshold: {
13 global: {
14 branches: 70,
15 functions: 70,
16 lines: 70,
17 statements: 70,
18 },
19 },
20};
typescript
1// __test__/globalSetup.ts
2import { MongoMemoryServer } from 'mongodb-memory-server';
3import mongoose from 'mongoose';
4
5let mongoServer: MongoMemoryServer;
6
7beforeAll(async () => {
8 mongoServer = await MongoMemoryServer.create();
9 const uri = mongoServer.getUri();
10 await mongoose.connect(uri);
11});
12
13afterAll(async () => {
14 await mongoose.disconnect();
15 await mongoServer.stop();
16});
17
18afterEach(async () => {
19 // Limpar collections após cada teste
20 const collections = mongoose.connection.collections;
21 await Promise.all(
22 Object.values(collections).map(collection => collection.deleteMany({}))
23 );
24});
Unit Tests: Services e Funções
Testar Lógica de Negócio Isoladamente
typescript
1// services/userService.test.ts
2import { UserService } from './userService';
3import { User } from '../models/User';
4
5describe('UserService', () => {
6 const userService = new UserService();
7
8 describe('createUser', () => {
9 it('should create user with hashed password', async () => {
10 const userData = {
11 name: 'Test User',
12 email: 'test@example.com',
13 password: 'password123',
14 };
15
16 const user = await userService.createUser(userData);
17
18 expect(user).toHaveProperty('_id');
19 expect(user.name).toBe('Test User');
20 expect(user.password).not.toBe('password123'); // Deve estar hasheado
21 expect(user.password).toHaveLength(60); // bcrypt hash length
22 });
23
24 it('should throw error if email already exists', async () => {
25 const userData = {
26 name: 'User',
27 email: 'duplicate@example.com',
28 password: 'password',
29 };
30
31 await userService.createUser(userData);
32
33 await expect(userService.createUser(userData))
34 .rejects.toThrow('Email already exists');
35 });
36 });
37
38 describe('findActiveUsers', () => {
39 beforeEach(async () => {
40 await User.create([
41 { name: 'Active 1', email: 'a1@test.com', active: true },
42 { name: 'Active 2', email: 'a2@test.com', active: true },
43 { name: 'Inactive', email: 'i1@test.com', active: false },
44 ]);
45 });
46
47 it('should return only active users', async () => {
48 const users = await userService.findActiveUsers();
49
50 expect(users).toHaveLength(2);
51 expect(users.every(u => u.active)).toBe(true);
52 });
53 });
54});
Integration Tests: API Endpoints
typescript
1// routes/users.test.ts
2import request from 'supertest';
3import app from '../app';
4import { User } from '../models/User';
5
6const API_BASE = '/api';
7const HTTP_OK = 200;
8const HTTP_CREATED = 201;
9const HTTP_BAD_REQUEST = 400;
10const HTTP_NOT_FOUND = 404;
11
12describe('User API', () => {
13 describe('POST /api/users', () => {
14 it('should create user and return 201', async () => {
15 const userData = {
16 name: 'Test User',
17 email: 'test@example.com',
18 password: 'password123',
19 };
20
21 const response = await request(app)
22 .post(`${API_BASE}/users`)
23 .send(userData);
24
25 expect(response.status).toBe(HTTP_CREATED);
26 expect(response.body.success).toBe(true);
27 expect(response.body.data).toHaveProperty('id');
28 expect(response.body.data.email).toBe(userData.email);
29 expect(response.body.data).not.toHaveProperty('password'); // Não deve retornar senha
30 });
31
32 it('should return 400 for invalid data', async () => {
33 const invalidData = {
34 name: '',
35 email: 'invalid-email',
36 password: '123', // muito curta
37 };
38
39 const response = await request(app)
40 .post(`${API_BASE}/users`)
41 .send(invalidData);
42
43 expect(response.status).toBe(HTTP_BAD_REQUEST);
44 expect(response.body.success).toBe(false);
45 expect(response.body.error).toBeTruthy();
46 });
47
48 it('should return 409 for duplicate email', async () => {
49 const userData = {
50 name: 'User',
51 email: 'duplicate@example.com',
52 password: 'password',
53 };
54
55 // Criar primeiro usuário
56 await request(app).post(`${API_BASE}/users`).send(userData);
57
58 // Tentar criar duplicado
59 const response = await request(app)
60 .post(`${API_BASE}/users`)
61 .send(userData);
62
63 expect(response.status).toBe(409);
64 expect(response.body.error).toMatch(/email already exists/i);
65 });
66 });
67
68 describe('GET /api/users/:id', () => {
69 it('should return user by id', async () => {
70 const user = await User.create({
71 name: 'Test',
72 email: 'test@example.com',
73 password: 'hashed',
74 });
75
76 const response = await request(app)
77 .get(`${API_BASE}/users/${user._id}`);
78
79 expect(response.status).toBe(HTTP_OK);
80 expect(response.body.data.id).toBe(user._id.toString());
81 expect(response.body.data.name).toBe('Test');
82 });
83
84 it('should return 404 for non-existent user', async () => {
85 const fakeId = '507f1f77bcf86cd799439011';
86
87 const response = await request(app)
88 .get(`${API_BASE}/users/${fakeId}`);
89
90 expect(response.status).toBe(HTTP_NOT_FOUND);
91 expect(response.body.error).toMatch(/not found/i);
92 });
93 });
94
95 describe('Authentication', () => {
96 it('should require auth token for protected routes', async () => {
97 const response = await request(app)
98 .get(`${API_BASE}/protected`);
99
100 expect(response.status).toBe(401);
101 });
102
103 it('should allow access with valid token', async () => {
104 const token = 'valid-jwt-token'; // Gerar token de teste
105
106 const response = await request(app)
107 .get(`${API_BASE}/protected`)
108 .set('Authorization', `Bearer ${token}`);
109
110 expect(response.status).toBe(HTTP_OK);
111 });
112 });
113});
Mocking
Mock de Serviços Externos
typescript
1// __test__/mocks/emailService.ts
2export const mockEmailService = {
3 sendEmail: vi.fn().mockResolvedValue(true),
4 sendWelcomeEmail: vi.fn().mockResolvedValue(true),
5};
6
7// Uso no teste
8vi.mock('@/services/emailService', () => ({
9 EmailService: vi.fn(() => mockEmailService),
10}));
11
12it('should send welcome email on registration', async () => {
13 const userData = { name: 'User', email: 'user@test.com', password: 'pass' };
14
15 await request(app).post('/api/users').send(userData);
16
17 expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
18 'user@test.com',
19 'User'
20 );
21});
Mock de MongoDB
typescript
1// Para testes sem MongoDB real
2vi.mock('mongoose', () => ({
3 connect: vi.fn(),
4 model: vi.fn(),
5 Schema: vi.fn(),
6}));
Best Practices
O que Testar
✅ TESTE:
- Lógica de negócio (validações, transformações)
- Endpoints de API (request/response)
- Error handling (casos de erro)
- Autenticação e autorização
- Database operations (CRUD)
- Integração com serviços externos
❌ NÃO TESTE:
- Bibliotecas third-party (já testadas)
- Código trivial (getters/setters simples)
- Configurações (env, setup básico)
Organização de Testes
__test__/
├── globalSetup.ts
├── fixtures/
│ └── users.json
├── mocks/
│ ├── emailService.ts
│ └── paymentGateway.ts
├── auth/
│ ├── login.test.ts
│ └── otp.test.ts
├── data/
│ └── api/
│ ├── find.test.ts
│ └── create.test.ts
└── utils/
└── dateUtils.test.ts
Naming Conventions
typescript
1describe('UserService', () => {
2 describe('createUser', () => {
3 it('should create user with valid data', async () => { });
4 it('should throw ValidationError for invalid data', async () => { });
5 it('should hash password before saving', async () => { });
6 });
7});
Checklist Rápido
Antes de commitar testes:
Recursos Adicionais
Filosofia: Teste comportamento da API, não implementação interna.