Testes tipados com Jest e ts-jest
1. Por que testar com TypeScript?
A tipagem estática em testes não é um luxo — é uma necessidade para projetos que almejam robustez. Quando escrevemos testes em JavaScript puro, erros como undefined is not a function só aparecem em runtime. Com TypeScript, muitos desses erros são capturados durante a compilação, antes mesmo do teste executar.
Considere a diferença entre um teste .js e .ts:
// JavaScript: erro só em runtime
const result = calculateTotal(undefined, 50);
// TypeError: Cannot read properties of undefined
// TypeScript: erro em tempo de compilação
const result = calculateTotal(undefined, 50);
// Argument of type 'undefined' is not assignable to parameter of type 'number'
O verdadeiro ganho surge quando a interface do seu código muda. Se um parâmetro de função for renomeado ou um tipo de retorno alterado, os testes tipados quebram imediatamente, forçando a atualização. Isso cria um ecossistema onde o sistema de tipos age como uma rede de segurança para refatorações.
2. Configurando o ambiente: Jest + ts-jest
Para começar, instale as dependências necessárias:
npm install --save-dev jest ts-jest @types/jest typescript
Crie um arquivo jest.config.ts na raiz do projeto:
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
};
export default config;
Ajuste o tsconfig.json para incluir os arquivos de teste:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"types": ["jest", "node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3. Escrevendo o primeiro teste tipado
Vamos testar uma função pura com parâmetros e retorno tipados:
// src/utils/math.ts
export function calculateDiscount(price: number, discountPercent: number): number {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Discount must be between 0 and 100');
}
return price * (1 - discountPercent / 100);
}
// src/__tests__/math.test.ts
import { calculateDiscount } from '../utils/math';
describe('calculateDiscount', () => {
it('should apply 20% discount correctly', () => {
const result: number = calculateDiscount(100, 20);
expect(result).toBe(80);
});
it('should throw error for invalid discount', () => {
expect(() => calculateDiscount(100, 150)).toThrow('Discount must be between 0 and 100');
});
it('should return same price for 0% discount', () => {
const result = calculateDiscount(100, 0);
expect(result).toBe(100);
});
});
Note como o TypeScript infere automaticamente os tipos de result e valida que calculateDiscount retorna um número.
4. Mocks e spies com tipagem forte
O uso de jest.fn() sem tipos pode introduzir any perigoso. Veja como tipar corretamente:
// src/services/email.ts
export interface EmailService {
send(to: string, subject: string, body: string): Promise<boolean>;
}
export class NotificationService {
constructor(private emailService: EmailService) {}
async notifyUser(email: string, message: string): Promise<boolean> {
return this.emailService.send(email, 'Notification', message);
}
}
// src/__tests__/notification.test.ts
import { NotificationService } from '../services/email';
import { jest } from '@jest/globals';
describe('NotificationService', () => {
it('should send notification successfully', async () => {
const mockSend = jest.fn<(to: string, subject: string, body: string) => Promise<boolean>>()
.mockResolvedValue(true);
const service = new NotificationService({ send: mockSend });
const result = await service.notifyUser('user@example.com', 'Hello!');
expect(result).toBe(true);
expect(mockSend).toHaveBeenCalledWith(
'user@example.com',
'Notification',
'Hello!'
);
});
});
Para mocks mais complexos, use jest.Mocked<T>:
import { jest } from '@jest/globals';
const mockEmailService: jest.Mocked<EmailService> = {
send: jest.fn().mockResolvedValue(true),
};
5. Testando classes e interfaces
Classes com dependências injetadas são testáveis com tipagem forte:
// src/models/user.ts
export interface User {
id: string;
name: string;
email: string;
}
export class UserRepository {
async findById(id: string): Promise<User | null> {
// Implementação real
return null;
}
}
export class UserService {
constructor(private repository: UserRepository) {}
async getUserEmail(id: string): Promise<string | null> {
const user = await this.repository.findById(id);
return user?.email ?? null;
}
}
// src/__tests__/user.test.ts
import { UserService, UserRepository } from '../models/user';
import { jest } from '@jest/globals';
describe('UserService', () => {
it('should return user email when user exists', async () => {
const mockUser = { id: '1', name: 'John', email: 'john@example.com' };
const mockFindById = jest.fn<(id: string) => Promise<typeof mockUser | null>>()
.mockResolvedValue(mockUser);
const repository = new UserRepository();
repository.findById = mockFindById;
const service = new UserService(repository);
const email = await service.getUserEmail('1');
expect(email).toBe('john@example.com');
});
});
6. Testes assíncronos e Promises tipadas
Funções assíncronas exigem tratamento cuidadoso de tipos:
// src/services/api.ts
interface ApiResponse<T> {
data: T;
status: number;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
const data: T = await response.json();
return { data, status: response.status };
}
// src/__tests__/api.test.ts
import { fetchData } from '../services/api';
describe('fetchData', () => {
it('should return typed response', async () => {
expect.assertions(2);
interface UserResponse {
id: number;
name: string;
}
const result = await fetchData<UserResponse>('/api/user/1');
expect(result.status).toBe(200);
expect(result.data.id).toBeDefined();
});
it('should handle errors with typed rejection', async () => {
await expect(fetchData('/invalid-url')).rejects.toThrow();
});
});
7. Integração com tsconfig paths e aliases
Para usar aliases de path, configure o tsconfig.json e o Jest:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"]
}
}
}
No jest.config.ts:
const config: Config = {
preset: 'ts-jest',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
},
};
Agora você pode importar com aliases nos testes:
import { calculateDiscount } from '@/utils/math';
8. Boas práticas e armadilhas comuns
Evite any e as unknown as T sempre que possível. Eles quebram a segurança de tipos:
// ❌ Ruim
const mockData: any = { name: 'John' };
// ✅ Bom
interface UserData { name: string; }
const mockData: UserData = { name: 'John' };
Mantenha mocks sincronizados com o código real. Se uma interface mudar, os mocks devem refletir essa mudança:
// ✅ Use tipos derivados
type MockEmailService = jest.Mocked<EmailService>;
Performance: Para projetos grandes, use isolatedModules: true no tsconfig.json dos testes:
{
"compilerOptions": {
"isolatedModules": true
}
}
Isso acelera a compilação, mas desabilita algumas verificações cross-file. Em contrapartida, a compilação completa com ts-jest oferece mais segurança de tipos.
Com essas práticas, seus testes não apenas verificam comportamento, mas também servem como documentação viva das interfaces do sistema. A tipagem forte transforma o conjunto de testes em uma barreira confiável contra regressões silenciosas.
Referências
- Documentação oficial do ts-jest — Guia completo de instalação, configuração e uso do ts-jest com Jest
- Jest Documentation: Getting Started with TypeScript — Tutorial oficial do Jest para configurar TypeScript
- TypeScript Testing with Jest - LogRocket Blog — Artigo prático sobre testes tipados com Jest e TypeScript
- Using Jest with TypeScript - Testing Library — Exemplos de configuração e boas práticas (embora focado em React, os conceitos de tipagem são universais)
- TypeScript Handbook: Testing — Seção oficial do TypeScript Handbook sobre estratégias de teste
- Mocking with Jest and TypeScript - Valerio Cipriani — Tutorial detalhado sobre mocks tipados com Jest