Introdução: Por que testar código TypeScript?
TypeScript é uma ferramenta poderosa que adiciona tipagem estática ao JavaScript, permitindo capturar erros em tempo de compilação. No entanto, mesmo o sistema de tipos mais rigoroso não garante que seu código funcionará corretamente em condições reais. Erros lógicos, tratamento incorreto de casos limite e problemas de integração de módulos permanecem fora do escopo do compilador. É aqui que os testes entram em ação.
Testar em TypeScript não é apenas verificar funções. É um processo abrangente que inclui três níveis principais: Unit (unitário), Integration (integração) e E2E (ponta a ponta). Cada um resolve um problema específico e, juntos, formam a "pirâmide de testes", garantindo a confiabilidade da aplicação.
Neste artigo, vamos explorar como escrever testes para TypeScript em todos os três níveis, usando ferramentas modernas: Jest, Supertest e Playwright. Você aprenderá quais erros esses testes ajudam a prevenir e como implementá-los em seu projeto.
Testes unitários: Verificação de módulos isolados
Testes unitários são a base da pirâmide. Eles testam funções, classes ou métodos individuais isolados de dependências externas (banco de dados, API, sistema de arquivos). O objetivo principal é garantir que cada "bloco" da sua aplicação funcione corretamente.
Ferramentas e configuração
O framework mais popular para testes unitários em TypeScript é o Jest. Ele oferece suporte nativo ao TypeScript através de ts-jest ou @swc/jest.
Instalação do Jest no projeto:
npm install --save-dev jest @types/jest ts-jestnpx ts-jest config:initExemplo de teste unitário
Vamos considerar uma função simples para calcular desconto:
// discount.tsexport function applyDiscount(price: number, discountPercent: number): number { if (discountPercent < 0 || discountPercent > 100) { throw new Error('A porcentagem de desconto deve estar entre 0 e 100'); } return price - (price * discountPercent) / 100;}Vamos escrever testes unitários cobrindo os cenários principais:
// discount.test.tsimport { applyDiscount } from './discount';
describe('applyDiscount', () => { it('deve aplicar corretamente um desconto de 20%', () => { expect(applyDiscount(100, 20)).toBe(80); });
it('deve retornar o preço original com desconto de 0%', () => { expect(applyDiscount(50, 0)).toBe(50); });
it('deve retornar 0 com desconto de 100%', () => { expect(applyDiscount(200, 100)).toBe(0); });
it('deve lançar erro para desconto negativo', () => { expect(() => applyDiscount(100, -10)).toThrow('A porcentagem de desconto deve estar entre 0 e 100'); });
it('deve lançar erro para desconto maior que 100%', () => { expect(() => applyDiscount(100, 150)).toThrow('A porcentagem de desconto deve estar entre 0 e 100'); });});Importante: Testes unitários não devem acessar recursos externos. Em vez disso, use mocks para substituir dependências, por exemplo, com jest.mock().
Testes de integração: Interação entre módulos
Testes de integração verificam como diferentes partes do sistema funcionam juntas. Pode ser a interação entre um serviço e um banco de dados, um controlador e um repositório, ou entre microsserviços. Diferente dos testes unitários, aqui não isolamos os componentes completamente.
Exemplo: testando uma API REST
Suponha que temos uma aplicação Express com um endpoint. Para testar requisições HTTP, usamos Supertest.
// userController.tsimport { Router, Request, Response } from 'express';
interface User { id: number; name: string;}
const users: User[] = [{ id: 1, name: 'Alice' }];