- Por que a maioria dos erros é de configuração
- Instalação que realmente funciona
- Estrutura de um teste: o padrão AAA
- O que merece ser testado — e o que é desperdício de tempo
- Async e Promises: a armadilha que ninguém avisa
- Mocks sem complicação
- Erros que aparecem em todo code review
- FAQ
- Próximos passos
Você instalou o Jest, escreveu o primeiro teste, rodou npx jest — e recebeu um erro que não tem nada a ver com o que você testou. É de módulo. Ou de transpiler. Ou de sintaxe. O teste nem chegou a rodar.
Isso acontece com todo mundo. O Jest tem uma curva de configuração real, especialmente em projetos TypeScript ou com ES Modules. Uma vez que isso está resolvido, o resto é direto ao ponto.
Por que a maioria dos erros é de configuração
O Jest surgiu numa época em que CommonJS era o padrão absoluto do Node.js. O ecossistema JavaScript mudou — e o Jest ainda carrega peso dessa herança. Quando você usa import/export nativo, TypeScript ou path aliases, o Jest não sabe como processar esses arquivos sem um transpiler configurado.
Os erros mais comuns logo no início:
SyntaxError: Cannot use import statement outside a module→ Jest rodando sem transformar ESMCannot find module '@/components/...'→ path alias não mapeado nomoduleNameMapperJest encountered an unexpected token→ JSX sem o preset do Babel ou semts-jest
Todos têm solução direta. O segredo é saber qual configuração usar para cada tipo de projeto.
Instalação que realmente funciona
Projeto JavaScript (CommonJS)
npm install --save-dev jest
No package.json:
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
Sem mais nada. Para CommonJS puro, funciona assim.
Projeto TypeScript — duas opções
Opção 1: ts-jest (mais popular)
npm install --save-dev jest ts-jest @types/jest
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
export default config;
Opção 2: @swc/jest (mais rápido)
Se os seus testes ficarem lentos com ts-jest, o @swc/jest é a alternativa. Usa o SWC como transpiler — compila TypeScript em Rust, o que costuma ser 10x mais rápido em projetos grandes.
npm install --save-dev jest @swc/jest @swc/core @types/jest
import type { Config } from 'jest';
const config: Config = {
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
testEnvironment: 'node',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
export default config;
Projeto Next.js
Use o helper oficial — ele resolve aliases, módulos de CSS e as configurações de ambiente automaticamente:
npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
const nextJest = require('next/jest');
const createJestConfig = nextJest({ dir: './' });
const config = {
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterFramework: ['<rootDir>/jest.setup.ts'],
};
module.exports = createJestConfig(config);
💡 Dica: A versão do
ts-jestprecisa ser compatível com a versão do Jest. Para Jest 29, usets-jest@^29. Incompatibilidade de versão é responsável por boa parte dos erros misteriosos que aparecem depois de umnpm update.
Estrutura de um teste: o padrão AAA
Todo teste é basicamente três coisas: preparar o contexto, executar a ação, verificar o resultado. O padrão AAA — Arrange, Act, Assert — deixa isso explícito.
export function calcularDesconto(preco: number, percentual: number): number {
if (percentual < 0 || percentual > 100) throw new Error('Percentual inválido');
return preco - (preco * percentual) / 100;
}
describe('calcularDesconto', () => {
it('aplica desconto de 10% corretamente', () => {
const preco = 200;
const percentual = 10;
const resultado = calcularDesconto(preco, percentual);
expect(resultado).toBe(180);
});
it('lança erro quando percentual é negativo', () => {
expect(() => calcularDesconto(100, -5)).toThrow('Percentual inválido');
});
it('lança erro quando percentual ultrapassa 100', () => {
expect(() => calcularDesconto(100, 150)).toThrow('Percentual inválido');
});
});
Quando o teste tem um nome que descreve comportamento e segue AAA, ele vira documentação. Daqui a seis meses, qualquer dev que abrir o arquivo vai entender o que calcularDesconto deve fazer sem precisar ler a implementação.
Matchers essenciais
expect(valor).toBe(42);
expect(objeto).toEqual({ id: 1, nome: 'Ana' });
expect(funcao).toThrow('mensagem de erro');
expect(array).toContain('item');
expect(string).toMatch(/padrão/);
expect(mock).toHaveBeenCalledWith('argumento');
expect(mock).toHaveBeenCalledTimes(2);
expect(numero).toBeGreaterThan(0);
expect(valor).toBeNull();
expect(valor).toBeDefined();
O toBe usa ===. Para objetos e arrays, use sempre toEqual — toBe vai falhar mesmo que os valores sejam idênticos, porque compara referência.
O que merece ser testado — e o que é desperdício de tempo
Teste unitário cobre lógica isolada. Parece óbvio, mas na prática aparece muito isso:
it('chama setLoading com true', () => {
expect(setLoading).toHaveBeenCalledWith(true);
});
Se você refatorar e remover o setLoading, o teste vai quebrar — mesmo que o comportamento externo continue igual. Isso é testar implementação, não comportamento. Quando o teste conhece os detalhes internos da função, ele se torna frágil.
O que testar:
- Lógica de negócio e regras de domínio
- Transformações e formatações de dados
- Validações com múltiplos casos (feliz, vazio, inválido, limite)
- Funções que calculam ou decidem algo
O que não compensa testar unitariamente:
- Getter e setter sem lógica
- Código que só repassa para uma biblioteca externa
- Componentes de UI sem lógica própria — para esses, testes de integração ou E2E fazem mais sentido
⚠️ Atenção: Coverage de 100% não garante que o sistema funciona. Já vi base de código com 95% de cobertura e bugs críticos em produção — porque os testes cobriam linhas, não cenários reais de uso.
Async e Promises: a armadilha que ninguém avisa
Testes assíncronos têm uma armadilha específica que pega bastante dev no começo: o Jest pode reportar o teste como passando mesmo quando ele deveria falhar.
it('busca usuário por ID', () => {
getUser(1).then(user => {
expect(user.nome).toBe('Ana');
});
});
Se getUser rejeitar a Promise, ou se o expect falhar, o Jest não vai capturar o erro — o teste já encerrou antes da Promise resolver. Resultado: falso positivo.
it('busca usuário por ID', async () => {
const user = await getUser(1);
expect(user.nome).toBe('Ana');
});
Sempre use async/await em testes assíncronos. Para testar rejeições:
it('lança erro quando usuário não existe', async () => {
await expect(getUser(9999)).rejects.toThrow('Usuário não encontrado');
});
Preparação e limpeza com beforeEach e afterEach
Quando vários testes precisam do mesmo estado inicial:
describe('serviço de pedidos', () => {
let pedidoService: PedidoService;
beforeEach(() => {
pedidoService = new PedidoService();
});
afterEach(() => {
jest.clearAllMocks();
});
it('cria pedido com status pendente', async () => {
const pedido = await pedidoService.criar({ produtoId: 1, quantidade: 2 });
expect(pedido.status).toBe('pendente');
});
it('rejeita pedido com quantidade zero', async () => {
await expect(
pedidoService.criar({ produtoId: 1, quantidade: 0 })
).rejects.toThrow('Quantidade inválida');
});
});
Mocks sem complicação
Mock serve para isolar dependências externas — banco, API, e-mail, sistema de arquivos. A confusão começa quando as pessoas usam mock para simular o próprio código delas.
Mockando um módulo
import { criarUsuario } from './usuario';
import * as emailService from './email';
jest.mock('./email');
it('envia e-mail de boas-vindas ao criar conta', async () => {
const mockEnviar = jest.spyOn(emailService, 'enviarBoasVindas')
.mockResolvedValue(undefined);
await criarUsuario({ nome: 'Ana', email: 'ana@example.com' });
expect(mockEnviar).toHaveBeenCalledWith('ana@example.com');
});
Diferença entre os três
| Método | Quando usar |
|---|---|
jest.fn() | Criar uma função fake do zero |
jest.mock('módulo') | Substituir um módulo inteiro por auto-mocks |
jest.spyOn(obj, 'método') | Monitorar (ou substituir) método existente num objeto |
Limpando o estado entre testes
Esse foi o tipo de bug que apareceu num PR de um colega — um teste passando sozinho, falhando quando rodado com os outros. O mock de um teste anterior estava vazando.
Configure globalmente no jest.config.ts:
const config: Config = {
clearMocks: true,
resetMocks: true,
restoreMocks: true,
};
clearMocks limpa histórico de chamadas. resetMocks reseta implementações customizadas. restoreMocks restaura spies ao original. Em quase todo projeto, os três juntos fazem sentido.
Erros que aparecem em todo code review
Só testar o caminho feliz
it('busca usuário', async () => {
const user = await getUser(1);
expect(user.nome).toBe('João');
});
it('lança erro quando ID não existe', async () => {
await expect(getUser(9999)).rejects.toThrow('não encontrado');
});
O segundo teste é tão importante quanto o primeiro. Sistemas falham nas bordas, não no meio.
Assertivas frágeis demais
expect(resultado.criadoEm).toBe('2024-01-15T10:30:00.000Z');
expect(resultado.criadoEm).toBeInstanceOf(Date);
expect(resultado.criadoEm.getTime()).toBeLessThanOrEqual(Date.now());
A string de data vai quebrar com qualquer mudança de timezone ou de fuso no CI. Teste o que importa: se é uma Date válida, se está no passado.
Nomes que não descrevem nada
it('test 1', () => { ... });
it('funciona corretamente', () => { ... });
it('retorna somente pedidos com status ativo', () => { ... });
it('ignora pedidos com data de expiração no passado', () => { ... });
O nome do teste é a primeira linha de diagnóstico quando algo quebra no CI. "test 1" não ajuda ninguém.
Esquecer os edge cases
Para toda função que processa listas ou strings:
it('retorna array vazio quando não há itens', () => {
expect(filtrarAtivos([])).toEqual([]);
});
it('não quebra com entrada nula', () => {
expect(() => filtrarAtivos(null as any)).not.toThrow();
});
FAQ
jest.mock(), jest.fn() e jest.spyOn(): qual usar em cada caso?
jest.fn() quando você precisa de uma função fake do zero, sem base numa implementação real. jest.mock('caminho/do/módulo') quando quer substituir o módulo inteiro — todos os exports viram mocks automáticos. jest.spyOn(objeto, 'método') quando você quer monitorar ou substituir um método que já existe, sem perder a implementação original (útil com mockResolvedValueOnce para simular uma falha pontual).
Meus testes estão lentos. Por onde começo?
Primeiro, verifique se algum teste está abrindo conexão real com banco ou chamando API externa — isso não deveria acontecer em testes unitários. Segundo, rode jest --runInBand para ver a ordem e identificar o gargalo. Terceiro, se o projeto é TypeScript, considere migrar de ts-jest para @swc/jest — a diferença em projetos maiores costuma ser significativa. Por último, revise setTimeout e substitua por jest.useFakeTimers().
Preciso de 100% de coverage para fazer deploy? Não. Coverage é um indicador, não uma meta. O número que faz sentido depende do projeto: módulos de lógica de negócio merecem 80%+, código de UI e glue code tem rendimento menor. O que importa mesmo é ter testes nos caminhos que realmente podem causar problema em produção.
Devo escrever os testes antes ou depois do código? As duas abordagens funcionam. TDD (test-first) tem a vantagem de forçar você a pensar na interface antes da implementação — isso melhora o design. Mas escrever o teste depois do código, enquanto o contexto ainda está fresco, já é muito melhor do que não testar. O pior cenário é acumular código sem teste e tentar cobrir tudo de uma vez.
Jest ou Vitest: qual usar em projeto novo? Em projetos Vite (Vue, React com Vite), Vitest é a escolha natural — configuração zero, compatível com a sintaxe do Jest, mais rápido por rodar na mesma pipeline do Vite. Em projetos Node.js puro, Next.js ou legacy, Jest ainda é mais seguro pela maturidade e pelo ecossistema de plugins. A API é quase idêntica, então migrar depois não é traumático.
Próximos passos
Se você ainda não tem nenhum teste rodando no projeto, o caminho mais rápido para sair do zero:
- Escolha uma função pura que já existe — algo que recebe parâmetros e retorna um valor, sem chamadas externas.
- Escreva três testes: o caso esperado, um edge case e um caso de erro.
- Rode
jest --coveragee olhe o relatório — não para obsessionar com o número, mas para identificar quais partes críticas ainda não têm cobertura. - A partir daí, escreva o teste junto com o código novo. Não precisa ser TDD; só não deixe acumular.
Testes unitários não são sobre cobertura de linha. São sobre a confiança de mudar código sem medo de quebrar o que já funciona. Quando você sente isso pela primeira vez — refatora uma função, roda os testes, tudo verde — o hábito começa a se justificar sozinho.