Testes Unitários
Testes garantem que seu código funciona corretamente e evitam regressões.
Setup com Jest
bash
npm install -D jest ts-jest @types/jestnpx ts-jest config:initjavascript
// jest.config.jsmodule.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/*.spec.ts', '**/*.test.ts'], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, collectCoverageFrom: [ 'src/**/*.ts', '!src/**/*.d.ts', '!src/server.ts', ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80 }, },};Scripts no package.json
json
{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }}Testando Funções Puras
typescript
// src/utils/price.tsexport function calculateDiscount(price: number, percentage: number): number { if (price < 0 || percentage < 0 || percentage > 100) { throw new Error('Valores inválidos'); } return price - (price * percentage / 100);} export function formatPrice(value: number): string { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', }).format(value);}typescript
// src/utils/price.spec.tsimport { calculateDiscount, formatPrice } from './price'; describe('calculateDiscount', () => { it('deve calcular desconto de 10%', () => { expect(calculateDiscount(100, 10)).toBe(90); }); it('deve calcular desconto de 50%', () => { expect(calculateDiscount(200, 50)).toBe(100); }); it('deve retornar mesmo valor para 0%', () => { expect(calculateDiscount(150, 0)).toBe(150); }); it('deve lançar erro para preço negativo', () => { expect(() => calculateDiscount(-100, 10)).toThrow('Valores inválidos'); }); it('deve lançar erro para porcentagem > 100', () => { expect(() => calculateDiscount(100, 150)).toThrow('Valores inválidos'); });}); describe('formatPrice', () => { it('deve formatar para BRL', () => { expect(formatPrice(1234.56)).toBe('R$ 1.234,56'); });});Mocking
typescript
// src/services/product.service.spec.tsimport { ProductService } from './product.service';import { prisma } from '../config/database'; // Mock do Prismajest.mock('../config/database', () => ({ prisma: { product: { findMany: jest.fn(), findUnique: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), }, },})); describe('ProductService', () => { let service: ProductService; beforeEach(() => { service = new ProductService(); jest.clearAllMocks(); }); describe('findAll', () => { it('deve retornar lista de produtos', async () => { const mockProducts = [ { id: '1', name: 'Produto 1', price: 100 }, { id: '2', name: 'Produto 2', price: 200 }, ]; (prisma.product.findMany as jest.Mock).mockResolvedValue(mockProducts); const result = await service.findAll(); expect(prisma.product.findMany).toHaveBeenCalled(); expect(result).toEqual(mockProducts); }); }); describe('findById', () => { it('deve retornar produto por id', async () => { const mockProduct = { id: '1', name: 'Produto 1', price: 100 }; (prisma.product.findUnique as jest.Mock).mockResolvedValue(mockProduct); const result = await service.findById('1'); expect(prisma.product.findUnique).toHaveBeenCalledWith({ where: { id: '1' }, }); expect(result).toEqual(mockProduct); }); it('deve retornar null se não encontrar', async () => { (prisma.product.findUnique as jest.Mock).mockResolvedValue(null); const result = await service.findById('999'); expect(result).toBeNull(); }); }); describe('create', () => { it('deve criar produto', async () => { const input = { name: 'Novo Produto', price: 150 }; const mockCreated = { id: '3', ...input }; (prisma.product.create as jest.Mock).mockResolvedValue(mockCreated); const result = await service.create(input); expect(prisma.product.create).toHaveBeenCalledWith({ data: input }); expect(result).toEqual(mockCreated); }); });});Testando Classes com Dependências
typescript
// src/services/order.service.spec.tsimport { OrderService } from './order.service';import { MailService } from './mail.service';import { PaymentService } from './payment.service'; // Mocks manuaisconst mockMailService = { sendOrderConfirmation: jest.fn(),}; const mockPaymentService = { processPayment: jest.fn(),}; describe('OrderService', () => { let service: OrderService; beforeEach(() => { service = new OrderService( mockMailService as unknown as MailService, mockPaymentService as unknown as PaymentService ); jest.clearAllMocks(); }); describe('checkout', () => { it('deve processar pagamento e enviar email', async () => { const orderData = { userId: '1', items: [], total: 100 }; mockPaymentService.processPayment.mockResolvedValue({ success: true }); mockMailService.sendOrderConfirmation.mockResolvedValue(undefined); await service.checkout(orderData); expect(mockPaymentService.processPayment).toHaveBeenCalled(); expect(mockMailService.sendOrderConfirmation).toHaveBeenCalled(); }); it('deve lançar erro se pagamento falhar', async () => { mockPaymentService.processPayment.mockResolvedValue({ success: false }); await expect(service.checkout({ total: 100 })) .rejects.toThrow('Pagamento não aprovado'); }); });});Resumo
- ✅ Jest configurado com TypeScript
- ✅ Testes de funções puras
- ✅ Mocking com jest.mock
- ✅ beforeEach para reset de estado
- ✅ Testes de casos de erro
Próxima aula: Testes de Integração! 🔗