Pular para o conteúdoPedro Farbo
Lição 17 / 2550 min

Testes Unitários com Jest

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:init
javascript
// 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! 🔗

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e