Testar TypeScript com Jest:
Configure Jest com TypeScript do jeito certo. De ts-jest e jest.config.ts até mocks tipados e testes assíncronos, tudo com exemplos práticos.
Por que isso é importante
Testar TypeScript com Jest:. Configure Jest com TypeScript do jeito certo. De ts-jest e jest.config.ts até mocks tipados e testes assíncronos, tudo com exemplos práticos.
Por Que Testar TypeScript com Jest
Jest é o framework de testes mais popular do ecossistema JavaScript. Roda testes em paralelo, tem mocks embutidos, cobertura de código nativa e uma API que qualquer dev aprende em minutos. O problema: ele foi feito pra JavaScript.
TypeScript precisa ser transpilado antes de executar. O Jest não sabe ler arquivos .ts nativamente. É aí que entra o ts-jest — um transformer que compila TypeScript on-the-fly durante a execução dos testes. Sem step de build separado.
A vantagem de testar com TypeScript é que seus mocks, fixtures e assertions também são tipados. Se uma função muda a assinatura, o teste quebra na compilação, não em runtime. Isso pega erros que em JavaScript só apareceriam quando o CI rodasse.
Como Configurar Jest com TypeScript Passo a Passo
Setup completo do zero. Cada passo te leva de um projeto sem testes a uma suíte rodando com tipagem completa.
Configuração Completa do jest.config.ts
Vamos à configuração que funciona em 95% dos projetos TypeScript.
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
// ts-jest transpila TypeScript on-the-fly
preset: 'ts-jest',
// 'node' pra backend, 'jsdom' pra frontend
testEnvironment: 'node',
// Onde ficam os testes
roots: ['<rootDir>/src'],
// Padrões de arquivo de teste
testMatch: [
'**/__tests__/**/*.test.ts',
'**/*.spec.ts',
],
// Path aliases (espelhar tsconfig.json)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
// Cobertura de código
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
// Limites de cobertura
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
export default config;
O coverageThreshold é opcional, mas recomendado. Ele faz o Jest falhar se a cobertura cair abaixo de 80%. Funciona como uma trava de segurança no CI: ninguém merga código que reduz a cobertura.
Escrevendo Testes Tipados
A grande vantagem de testar com TypeScript é ter tipagem nos testes. Veja como isso funciona na prática.
Teste Básico com Tipagem
// src/utils/math.ts
export function soma(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Divisão por zero');
return a / b;
}
// src/utils/__tests__/math.test.ts
import { soma, divide } from '../math';
describe('math utils', () => {
describe('soma', () => {
it('soma dois números positivos', () => {
const resultado: number = soma(2, 3);
expect(resultado).toBe(5);
});
it('soma números negativos', () => {
expect(soma(-1, -2)).toBe(-3);
});
it('soma com zero', () => {
expect(soma(5, 0)).toBe(5);
});
});
describe('divide', () => {
it('divide corretamente', () => {
expect(divide(10, 2)).toBe(5);
});
it('lança erro ao dividir por zero', () => {
expect(() => divide(10, 0)).toThrow('Divisão por zero');
});
});
});
Testando Interfaces e Tipos Complexos
// src/models/user.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
export function createUser(data: Omit<User, 'id'>): User {
return {
id: crypto.randomUUID(),
...data,
};
}
export function isAdmin(user: User): boolean {
return user.role === 'admin';
}
// src/models/__tests__/user.test.ts
import { User, createUser, isAdmin } from '../user';
describe('User model', () => {
const mockUserData: Omit<User, 'id'> = {
name: 'Maria Silva',
email: 'maria@email.com',
role: 'admin',
};
it('cria usuário com id gerado', () => {
const user = createUser(mockUserData);
expect(user.id).toBeDefined();
expect(user.name).toBe('Maria Silva');
expect(user.email).toBe('maria@email.com');
expect(user.role).toBe('admin');
});
it('identifica admin corretamente', () => {
const admin = createUser({ ...mockUserData, role: 'admin' });
const regular = createUser({ ...mockUserData, role: 'user' });
expect(isAdmin(admin)).toBe(true);
expect(isAdmin(regular)).toBe(false);
});
});
Repare como os tipos guiam os testes. Se createUser mudar a interface, os testes quebram na compilação, não na execução. Esse feedback rápido é o que faz TypeScript + Jest ser tão produtivo.
Mocks Tipados no Jest
Mocks são a parte mais confusa de testar com TypeScript. O Jest tem jest.fn() e jest.mock(), mas sem tipagem eles perdem toda a segurança. Veja como tipar mocks corretamente.
// src/services/email.ts
export interface EmailService {
send(to: string, subject: string, body: string): Promise<boolean>;
}
// src/services/notification.ts
import { EmailService } from './email';
export class NotificationService {
constructor(private emailService: EmailService) {}
async notifyUser(email: string, message: string): Promise<boolean> {
return this.emailService.send(
email,
'Nova Notificação',
message
);
}
}
// src/services/__tests__/notification.test.ts
import { NotificationService } from '../notification';
import { EmailService } from '../email';
describe('NotificationService', () => {
// Mock tipado da interface EmailService
const mockEmailService: jest.Mocked<EmailService> = {
send: jest.fn(),
};
const service = new NotificationService(mockEmailService);
beforeEach(() => {
jest.clearAllMocks();
});
it('envia email com dados corretos', async () => {
mockEmailService.send.mockResolvedValue(true);
const result = await service.notifyUser(
'user@email.com',
'Olá!'
);
expect(result).toBe(true);
expect(mockEmailService.send).toHaveBeenCalledWith(
'user@email.com',
'Nova Notificação',
'Olá!'
);
});
it('retorna false quando email falha', async () => {
mockEmailService.send.mockResolvedValue(false);
const result = await service.notifyUser(
'user@email.com',
'Teste'
);
expect(result).toBe(false);
});
});
O jest.Mocked<EmailService> é a chave. Ele transforma todos os métodos da interface em jest.Mock tipados. Quando você chama mockResolvedValue, o TypeScript garante que o valor de retorno bate com o tipo original. Se send retorna Promise<boolean>, o mock também precisa retornar boolean.
Testando Funções Assíncronas
A maioria das funções reais é assíncrona: chamadas de API, queries no banco, leitura de arquivos. Testar async/await com Jest e TypeScript exige atenção com tipos e tratamento de erro.
// src/api/users.ts
import { User } from '@/models/user';
export async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Usuário ${id} não encontrado`);
}
return response.json();
}
export async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
return response.json();
}
// src/api/__tests__/users.test.ts
import { fetchUser, fetchUsers } from '../users';
// Mock global do fetch
global.fetch = jest.fn();
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
describe('users API', () => {
beforeEach(() => {
mockFetch.mockReset();
});
describe('fetchUser', () => {
it('retorna usuário quando API responde 200', async () => {
const mockUser = {
id: '1',
name: 'João',
email: 'joao@email.com',
role: 'user' as const,
};
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockUser,
} as Response);
const user = await fetchUser('1');
expect(user).toEqual(mockUser);
expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
});
it('lança erro quando API responde 404', async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 404,
} as Response);
await expect(fetchUser('999'))
.rejects
.toThrow('Usuário 999 não encontrado');
});
});
describe('fetchUsers', () => {
it('retorna lista de usuários', async () => {
const mockUsers = [
{ id: '1', name: 'Ana', email: 'ana@email.com', role: 'admin' },
{ id: '2', name: 'Pedro', email: 'pedro@email.com', role: 'user' },
];
mockFetch.mockResolvedValue({
ok: true,
json: async () => mockUsers,
} as Response);
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(users[0].name).toBe('Ana');
});
});
});
O padrão expect(...).rejects.toThrow() testa erros em funções async sem precisar de try/catch no teste. Limpo e direto. Use mockResolvedValue pra simular respostas de sucesso e mockRejectedValue pra simular falhas de rede.
Utilities de Teste Tipadas
Conforme o projeto cresce, você repete código nos testes: fixtures, factories, helpers. Criar utilities tipadas centraliza essa lógica e garante consistência.
// src/__tests__/helpers/factories.ts
import { User } from '@/models/user';
// Factory com valores padrão e override tipado
export function createMockUser(
overrides: Partial<User> = {}
): User {
return {
id: 'test-id-123',
name: 'Test User',
email: 'test@email.com',
role: 'user',
...overrides,
};
}
// Factory pra listas
export function createMockUsers(count: number): User[] {
return Array.from({ length: count }, (_, i) =>
createMockUser({
id: `test-id-${i}`,
name: `User ${i}`,
email: `user${i}@email.com`,
})
);
}
// Uso nos testes:
import { createMockUser, createMockUsers } from '../helpers/factories';
it('processa admin corretamente', () => {
// Override só o que importa pro teste
const admin = createMockUser({ role: 'admin' });
expect(isAdmin(admin)).toBe(true);
});
it('lista 10 usuários', () => {
const users = createMockUsers(10);
expect(users).toHaveLength(10);
expect(users[0].email).toBe('user0@email.com');
});
Factories com Partial<T> são poderosas. Nos testes, você só passa os campos relevantes pro cenário sendo testado. Os outros ficam com valores padrão sensatos. Menos código repetido, mais clareza no que cada teste valida.
Erros Comuns ao Testar TypeScript com Jest
Armadilhas nos testes
Não instalar @types/jest: sem esse pacote, describe, it e expect aparecem como undefined. O TypeScript não sabe que esses globais existem sem os types.
Usar jest.fn() sem tipar: jest.fn() retorna jest.Mock<any, any>. Você perde toda a segurança. Use jest.fn<ReturnType, [ParamTypes]>() ou jest.Mocked<Interface> pra manter tipos.
Esquecer de limpar mocks entre testes: sem beforeEach(() => jest.clearAllMocks()), os mocks acumulam chamadas. Um teste interfere no outro e os resultados ficam inconsistentes.
Mock de módulo com tipagem errada: jest.mock('./modulo') faz o mock funcionar, mas os tipos do import original continuam valendo. Cast o import com as jest.Mocked<typeof modulo> pra ter os métodos de mock disponíveis.
Não tratar rejeições de Promise: se uma função async lança erro e o teste não trata com rejects.toThrow(), o Jest marca como falha por timeout em vez de mostrar o erro real. Sempre teste os cenários de erro.
Checklist de Testes TypeScript com Jest
Testes Profissionais em TypeScript
Testar código TypeScript com Jest é uma skill que separa devs júniors de profissionais. No CrazyStack, cada módulo tem testes tipados — mocks, factories, integração. Você aprende a construir uma suíte de testes que dá confiança real pra refatorar e fazer deploy.
Código testado é código confiável. Se quer evoluir como dev, testes são o caminho mais curto.
Continue lendo
Como Usar Strict Mode no TypeScript
Ative o strict mode e elimine categorias inteiras de bugs no TypeScript.
Como Configurar Path Aliases no TypeScript
Configure path aliases no tsconfig e deixe seus imports limpos.
Como Configurar ESLint com TypeScript
Setup completo de ESLint para projetos TypeScript com regras que funcionam.
Interface no TypeScript
Quando usar interface