Estratégia de testes no front-end: unitários, integração e E2E
Testes unitários, de integração e E2E no front-end com Vitest, React Testing Library, MSW e Playwright, e como escolher o nível de cada comportamento.
intermediárioPublicada em · 24 min de leitura
Um teste existe para responder a uma pergunta: “se alguém mudar este código amanhã, algo que importa vai quebrar sem que eu perceba?”. Quase todo o trabalho de testar está em escolher o que vale a pena perguntar e em que nível perguntar. Testar tudo com a mesma ferramenta gera uma suíte lenta, frágil e que ninguém confia. Testar pouco deixa o usuário descobrir os bugs.
Esta aula mostra como pensar essa escolha, e depois mostra como escrever cada tipo de teste com as ferramentas mais usadas em 2026: Vitest para rodar os testes, React Testing Library para montar componentes do jeito que o usuário os usa, MSW para simular a API e Playwright para testar o sistema inteiro num navegador de verdade. Os exemplos usam React e TypeScript, mas os princípios valem para Vue, Svelte ou Angular.
Este é um texto de estudo. Os trechos de código mostram o formato e as escolhas, e não foram executados dentro do vault. Antes de copiar um exemplo para um projeto, rode-o e confira a versão das bibliotecas do seu projeto.
Preparação do projeto
Antes dos exemplos, o projeto precisa de uma configuração mínima. O Vitest roda os testes e precisa de um ambiente de DOM simulado para os componentes. O arquivo abaixo cuida disso e ignora a pasta dos testes E2E, porque o Playwright usa outro executor e o Vitest não deve tentar rodá-los:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
exclude: ["e2e/**", "node_modules/**"],
},
});
Sem essa exclusão, o Vitest tenta executar os arquivos *.spec.ts da pasta e2e e falha com a mensagem de que test() não era esperado ali. As bibliotecas usadas são vitest, jsdom, @testing-library/react, @testing-library/user-event, @testing-library/jest-dom, msw, @tanstack/react-query e @playwright/test.
Parte 1: o que um teste prova
1. Comportamento, não implementação
Imagine um botão “Adicionar ao carrinho” que, ao ser clicado, soma um item e mostra a mensagem “Item adicionado”. Existem dois jeitos de testar isso.
O primeiro jeito olha para dentro do componente: verifica se a função setQuantidade foi chamada com o valor 1, se o estado interno mudou de 0 para 1, se a classe CSS botao-ativo foi aplicada. O teste passa enquanto o código tiver esses detalhes exatos.
O segundo jeito olha para o que o usuário vê: clica no botão e espera ver a mensagem na tela. O teste não sabe como o estado é guardado, nem se a contagem vem do Redux, do Zustand ou de um useState.
A diferença importa. Quando você refatora o componente sem mudar o que o usuário percebe, o primeiro teste quebra, e o segundo continua passando. Um teste que quebra sem que nada visível tenha mudado ensina a equipe a ignorar falhas, e esse é o pior resultado possível para uma suíte.
A regra prática é testar entradas e saídas observáveis: o que o usuário faz, o que aparece na tela, o que é enviado para a API, o que muda na URL. Detalhes internos só entram quando são o próprio assunto, como uma função pura de cálculo.
Pergunta de entrevistaPor que você testa comportamento e não implementação?
Um teste de comportamento verifica o que o usuário percebe: o que ele faz, o que aparece na tela, o que vai para a API. Um teste de implementação verifica detalhes internos, como se setQuantidade foi chamada ou se uma classe CSS foi aplicada. No botão “Adicionar ao carrinho”, eu clico e espero ver “Item adicionado”, sem saber se o estado vem de useState, Zustand ou Redux. Assim, quando refatoro o componente sem mudar o que o usuário vê, o teste continua passando. O erro comum é acoplar o teste à estrutura interna. Ele quebra sem que nada visível tenha mudado, e a equipe aprende a ignorar falhas, que é o pior resultado para uma suíte. Detalhes internos só entram quando são o próprio assunto, como uma função pura de cálculo.
2. Três níveis e o que cada um cobre
Os testes costumam ser divididos em três níveis, cada um com um papel diferente.
Teste unitário verifica uma unidade isolada: uma função, um hook, um utilitário de formatação. Ele é rápido, roda em milissegundos e falha com uma causa clara, porque só há uma coisa em jogo.
Teste de integração verifica que várias partes funcionam juntas: um formulário conectado a um hook, um componente que busca dados de uma API, uma tela com contexto de autenticação. Ele é um pouco mais lento e exige mais preparação, mas cobre as ligações que costumam falhar, como um campo que não envia o valor certo ou um estado que não atualiza depois da resposta.
Teste E2E, de ponta a ponta, sobe a aplicação real, abre um navegador e percorre um fluxo como um usuário faria: entrar, comprar, sair. Ele é o mais lento e o mais caro de manter, mas é o único que garante que o sistema inteiro funciona na prática, com roteamento, cookies, build e backend.
3. Pirâmide ou troféu
Durante muito tempo a recomendação foi a pirâmide clássica: muitos testes unitários na base, uma quantidade menor de integração no meio e poucos E2E no topo. A lógica é econômica: testes baixos são baratos, e testes altos são caros.
No front-end, a pirâmide tem um problema. Boa parte do que dá errado está nas ligações entre componentes, hooks e a API, e um teste unitário isolado não enxerga essas ligações. Por isso muitos times adotam o formato de troféu, popularizado por Kent C. Dodds: uma base pequena de testes estáticos (tipos e lint), uma parte unitária menor, um corpo grande de testes de integração e uma pequena camada de E2E.
Os dois modelos concordam em um ponto: não há uma proporção universal. A pergunta correta é onde o risco está. Se o seu projeto tem muitas funções de cálculo fiscal, o peso vai para o unitário. Se ele é quase toda interface que conversa com uma API, o peso vai para a integração.
Pergunta de entrevistaPirâmide de testes ou troféu: qual você adotaria no front-end?
A pirâmide pede muitos testes unitários na base, menos de integração e poucos E2E no topo, por uma lógica de custo: testes baixos são baratos. O troféu, popularizado por Kent C. Dodds, tem uma base de testes estáticos (tipos e lint), poucos unitários, muita integração e uma camada pequena de E2E. No front-end eu me inclino ao troféu quando a interface conversa muito com uma API, porque boa parte dos bugs está nas ligações entre componentes, hooks e dados, e um teste unitário isolado não as enxerga. Mas não existe proporção universal. Eu pergunto onde o risco está. Num projeto com muito cálculo fiscal, o peso vai para o unitário.
Parte 2: testes unitários
4. Quando um teste unitário compensa
Teste unitário compensa quando a lógica é pura ou quase pura: uma função que calcula preço, um formatador de data, um reducer, um validador de CPF, uma função que transforma a resposta da API em um modelo de tela. Nesses casos, a entrada e a saída são claras, não há rede, não há DOM, e os casos de borda são fáceis de listar.
Não compensa para cada componente visual. Testar que um <Botao> tem a cor certa é, quase sempre, teste de aparência, e aparência se verifica melhor com revisão visual ou teste de regressão visual.
5. Um exemplo com a regra de desconto
Suponha esta função:
// precos.ts
export function aplicarDesconto(preco: number, percentual: number): number {
if (percentual < 0 || percentual > 100) {
throw new RangeError("percentual deve estar entre 0 e 100");
}
return Math.round(preco * (1 - percentual / 100) * 100) / 100;
}
Os testes precisam cobrir os casos normais, os limites e o erro. O it.each evita repetir o mesmo teste com valores diferentes, e cada linha da tabela documenta um caso:
// precos.test.ts
import { describe, expect, it } from "vitest";
import { aplicarDesconto } from "./precos";
describe("aplicarDesconto", () => {
it.each([
[100, 0, 100],
[100, 10, 90],
[59.9, 50, 29.95],
])("preço %f com desconto de %i por cento resulta em %f", (preco, percentual, esperado) => {
expect(aplicarDesconto(preco, percentual)).toBe(esperado);
});
it("recusa percentual fora do intervalo", () => {
expect(() => aplicarDesconto(100, 101)).toThrow(RangeError);
});
});
Repare no que o teste não faz. Ele não verifica como a função é escrita por dentro. Se amanhã o cálculo for trocado por outra fórmula equivalente, o teste continua valendo, porque a regra de negócio é a mesma.
6. Mock: quando usar e quando evitar
Mock é um substituto controlado para uma dependência. Em Vitest, vi.fn() cria uma função espiã, e vi.mock() troca um módulo inteiro.
Mock é útil para isolar o que está fora do seu controle: relógio do sistema, gerador de números aleatórios, um serviço de e-mail, uma biblioteca externa com efeitos colaterais. Ele é perigoso quando substitui a própria lógica que você quer testar. Se você mocka o módulo de cálculo para testar o componente que o usa, o teste passa mesmo que o cálculo esteja errado.
Uma regra útil: mocke a fronteira, não o interior. Se a sua função chama fetch, o mock deve ficar na fronteira da rede, e é exatamente isso que o MSW faz na Parte 3. Mockar cada função interna cria testes que só provam que o código foi escrito como você o escreveu.
Pergunta de entrevistaQuando você usa mock em um teste e quando evita?
Mock é um substituto controlado para uma dependência. Eu uso para isolar o que está fora do meu controle: relógio, números aleatórios, um serviço de e-mail ou uma biblioteca externa com efeitos colaterais. Evito quando ele substitui a própria lógica que quero testar. Se eu mocko o módulo de cálculo para testar o componente que o usa, o teste passa mesmo com o cálculo errado. Minha regra é mockar a fronteira, não o interior. Se a função chama fetch, o mock fica na fronteira da rede, e é isso que o MSW faz. Mockar cada função interna gera testes que só provam que o código foi escrito do jeito que eu o escrevi.
Parte 3: testes de componente e de integração
7. Montar o componente como o usuário o usa
O React Testing Library renderiza o componente num DOM simulado e oferece consultas que procuram elementos pelo que o usuário enxerga: papel acessível, rótulo, texto. A ordem de preferência está detalhada em Queries, e a regra central é: se o seu teste não consegue achar o elemento pelo papel ou pelo rótulo, provavelmente o usuário de leitores de tela também não consegue. Por isso, o teste ajuda a melhorar a acessibilidade. Veja Acessibilidade nos testes.
Veja um formulário de login. Ele recebe uma função onEntrar e a chama com os dados digitados:
// FormularioDeLogin.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, it, vi } from "vitest";
import { FormularioDeLogin } from "./FormularioDeLogin";
it("envia e-mail e senha digitados", async () => {
const user = userEvent.setup();
const onEntrar = vi.fn();
render(<FormularioDeLogin onEntrar={onEntrar} />);
await user.type(screen.getByLabelText("E-mail"), "ana@exemplo.com");
await user.type(screen.getByLabelText("Senha"), "segredo123");
await user.click(screen.getByRole("button", { name: "Entrar" }));
expect(onEntrar).toHaveBeenCalledWith({
email: "ana@exemplo.com",
senha: "segredo123",
});
});
Três escolhas merecem atenção. O userEvent.setup() simula o usuário com eventos completos, como foco, digitação caractere a caractere e clique, e é mais fiel do que fireEvent. O getByLabelText encontra o campo pelo rótulo visível, que é o que o usuário lê. E o expect verifica o efeito observável, a chamada para a ação de login, sem olhar para o estado interno do formulário. Detalhes do userEvent estão em User Events.
8. Integração com a API: MSW no lugar do fetch
Um componente que busca dados está no meio do caminho entre unitário e E2E. O jeito antigo de testá-lo era mockar o módulo de API inteiro, por exemplo vi.mock("./api"). O problema é que o componente deixava de usar a rede de verdade, e qualquer mudança na forma de chamar a API passava sem ser notada.
O MSW (Mock Service Worker) intercepta a requisição no nível da rede simulada. O componente faz o fetch normalmente, e o MSW devolve a resposta que você definiu. Assim, o código de chamada, de tratamento de erro e de atualização da tela roda de verdade.
// servidor.ts
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
export const servidor = setupServer(
http.get("https://api.exemplo.com/produtos", () =>
HttpResponse.json([{ id: 1, nome: "Caneca" }]),
),
);
No arquivo de configuração de testes, o servidor sobe antes da suíte, é limpo entre os testes e desliga no fim. A primeira linha registra os matchers do jest-dom, como toHaveTextContent, que os exemplos usam mais adiante:
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { afterAll, afterEach, beforeAll } from "vitest";
import { servidor } from "./servidor";
beforeAll(() => servidor.listen({ onUnhandledFrame: "error" }));
afterEach(() => servidor.resetHandlers());
afterAll(() => servidor.close());
A opção onUnhandledFrame: "error" faz o teste falhar quando o componente chama uma URL que você não configurou. Sem ela, a chamada escapa para a rede real e o teste passa ou falha por motivos que ninguém controla. Atenção ao nome: no MSW 2 a opção se chamava onUnhandledRequest, e no MSW 3 virou onUnhandledFrame. Confira a versão instalada no seu projeto, porque com o nome antigo o TypeScript reclama da opção.
Agora o caso de erro, que é o que costuma faltar nas suítes. O server.use troca o handler só para este teste. A função renderizarComProviders, da seção 9, é usada aqui porque o componente consulta a API pelo TanStack Query:
// ListaDeProdutos.test.tsx
import { screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { expect, it } from "vitest";
import { servidor } from "./servidor";
import { ListaDeProdutos } from "./ListaDeProdutos";
import { renderizarComProviders } from "./renderizar";
it("mostra mensagem quando a API falha", async () => {
servidor.use(
http.get("https://api.exemplo.com/produtos", () =>
new HttpResponse(null, { status: 500 }),
),
);
renderizarComProviders(<ListaDeProdutos />);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Não foi possível carregar os produtos",
);
});
O findByRole espera o elemento aparecer, porque a resposta chega de forma assíncrona. Use findBy para algo que vai aparecer depois, e getBy só para algo que já está na tela. Essa diferença resolve a maior parte dos testes instáveis de integração.
Pergunta de entrevistaPor que usar MSW em vez de mockar o módulo da API?
Quando mocko o módulo de API com vi.mock("./api"), o componente deixa de usar a rede de verdade. Uma mudança na forma de chamar a API passa sem ser notada. O MSW intercepta a requisição na camada de rede: o componente faz o fetch normalmente e recebe a resposta que defini, então o código de chamada, o tratamento de erro e a atualização da tela rodam de verdade. Na configuração, uso onUnhandledFrame: "error" para o teste falhar se o componente chamar uma URL que não configurei, em vez de escapar para a rede real. Para o caso de erro, troco o handler só naquele teste com servidor.use, devolvendo status 500. Também confiro a versão do MSW, porque o nome dessa opção mudou entre versões.
9. Estado global e providers
Muitos componentes precisam de um provider para funcionar: um QueryClientProvider do TanStack Query, um contexto de tema, um roteador. Em vez de repetir o provider em cada teste, crie uma função de renderização que já inclui tudo:
// renderizar.tsx
import { render } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactElement } from "react";
export function renderizarComProviders(ui: ReactElement) {
const cliente = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={cliente}>{ui}</QueryClientProvider>,
);
}
O retry: false é importante. Por padrão, o TanStack Query tenta de novo quando uma requisição falha, e o teste de erro ficaria esperando tentativas que não vão acontecer no ambiente de teste. Um cliente novo a cada teste também evita que um dado de um teste vaze para o próximo.
10. O que evitar nos testes de integração
Os erros mais comuns nessa camada são de três tipos. O primeiro é usar setTimeout ou waitFor com tempo fixo para esperar algo, quando o correto é esperar um elemento ou um efeito observável. O segundo é compartilhar estado entre testes, como uma variável global ou um banco em memória que não é limpo. O terceiro é mockar tanto que o teste deixa de exercitar a integração que ele deveria cobrir.
Snapshot merece um aviso à parte. Um snapshot de uma árvore de componentes grande muda a cada pequena alteração de marcação, e a equipe acaba atualizando o snapshot sem olhar o diff. Se usar snapshot, mantenha-o pequeno e focado num trecho com significado.
Parte 4: testes E2E com Playwright
11. Quando vale o custo do E2E
O E2E custa caro: é lento, depende de build e servidor, pode ter instabilidade de rede e exige manutenção quando a interface muda. Por isso ele deve ser reservado para os fluxos que têm valor de negócio direto e que dependem da integração completa: cadastro, login, checkout, pagamento, fluxo de recuperação de senha.
Não é necessário um E2E para cada tela. Se a validação de um campo de CEP é uma regra de formulário, um teste de integração resolve com mais precisão e em uma fração do tempo.
Pergunta de entrevistaQuando vale o custo de um teste E2E?
O E2E sobe a aplicação real num navegador, então é lento, depende de build e servidor, pode sofrer com instabilidade de rede e exige manutenção quando a interface muda. Por isso eu o reservo para fluxos com valor direto de negócio que dependem da integração completa, como cadastro, login, checkout, pagamento e recuperação de senha. Não faço um E2E para cada tela. Se a validação de um campo de CEP é uma regra de formulário, um teste de integração cobre com mais precisão e em uma fração do tempo. O erro comum é empilhar E2E para tudo, o que deixa a suíte lenta e cara de manter sem aumentar a confiança proporcionalmente.
12. Configuração básica
O Playwright roda em navegadores reais, pode subir o servidor da aplicação antes dos testes e captura um registro detalhado quando algo falha:
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});
O trace: "on-first-retry" grava uma linha do tempo com capturas, rede e console só na segunda tentativa de um teste que falhou. Isso mantém os testes que passam leves e deixa o diagnóstico disponível quando algo quebra no CI.
13. Um fluxo de compra
Veja um teste de fluxo completo:
// e2e/compra.spec.ts
import { expect, test } from "@playwright/test";
test("cliente finaliza uma compra", async ({ page }) => {
await page.goto("/produtos");
await page
.getByRole("button", { name: "Adicionar ao carrinho" })
.first()
.click();
await page.getByRole("link", { name: "Carrinho" }).click();
await page.getByRole("button", { name: "Finalizar compra" }).click();
await expect(
page.getByRole("heading", { name: "Pedido confirmado" }),
).toBeVisible();
});
Os locators do Playwright seguem a mesma filosofia do React Testing Library: buscam por papel e por texto visível. E o expect do Playwright espera automaticamente até a condição ser verdadeira, dentro de um limite de tempo. Por isso não há waitForTimeout no exemplo. Um tempo fixo de espera é a principal causa de testes que passam em uma máquina e falham em outra.
14. Dados e login sem repetição
Um E2E que faz login pelo formulário em todos os testes fica lento e repete a mesma etapa dezenas de vezes. A alternativa é fazer o login uma vez num teste de preparação, salvar o estado do navegador (cookies e armazenamento local) num arquivo e reutilizá-lo nos demais. O Playwright chama isso de storageState.
O cuidado é com os dados. Um E2E que cria um pedido em um banco compartilhado pode quebrar outro que espera a lista vazia. Crie os dados de que cada teste precisa, use um usuário exclusivo para o teste e, se possível, rode contra um ambiente de teste com banco descartável. Com isso, a ordem dos testes deixa de importar.
Pergunta de entrevistaComo você evita repetir login e dados compartilhados nos testes E2E?
Fazer login pelo formulário em todo teste deixa a suíte lenta e repete a mesma etapa dezenas de vezes. No Playwright, faço o login uma vez num teste de preparação, salvo o estado do navegador (cookies e armazenamento local) com storageState e reaproveito nos demais. Os dados pedem outro cuidado. Um E2E que cria um pedido em um banco compartilhado pode quebrar outro que espera a lista vazia. Então cada teste cria os dados de que precisa, usa um usuário exclusivo e, se possível, roda contra um ambiente com banco descartável. Com isso, a ordem de execução deixa de importar, e os testes não dependem uns dos outros.
15. Instabilidade: o inimigo principal do E2E
Um teste instável, chamado de flaky, passa e falha sem mudança no código. Ele mina a confiança da equipe mais rápido do que a ausência de teste. As causas mais comuns são espera por tempo fixo, seletores frágeis que dependem da posição do elemento, dados compartilhados entre testes e animações que mudam a posição do alvo no meio do clique.
A correção vem de três hábitos. Use locators por papel e rótulo, que sobrevivem a mudanças de layout. Espere condições, não tempos. E configure retries no CI para que uma falha isolada não bloqueie o merge, registrando quais testes precisaram de nova tentativa. A repetição é uma rede de segurança, e não uma solução: se um teste só passa na segunda tentativa com frequência, ele está escondendo um problema que precisa ser corrigido.
Pergunta de entrevistaComo você lida com testes instáveis (flaky) no E2E?
Um teste flaky passa e falha sem mudança no código, e isso corrói a confiança da equipe mais rápido do que a falta de teste. As causas comuns são espera por tempo fixo, seletores que dependem da posição do elemento, dados compartilhados entre testes e animações que mexem no alvo durante o clique. Eu uso locators por papel e rótulo, espero condições em vez de tempos (o expect do Playwright já espera automaticamente) e isolo os dados. No CI, configuro retries para uma falha isolada não bloquear o merge e registro quais testes precisaram de nova tentativa. A repetição é rede de segurança, não solução: um teste que só passa na segunda tentativa com frequência esconde um problema que precisa ser corrigido.
Parte 5: como escolher o nível
16. Uma tabela de decisão
| Pergunta | Unitário | Integração | E2E |
|---|---|---|---|
| O que ele cobre? | Uma função ou hook | Componentes e a camada de dados juntos | O sistema inteiro num navegador |
| Velocidade | Milissegundos | Dezenas a centenas de milissegundos | Segundos por teste |
| Ferramenta típica | Vitest | Vitest + React Testing Library + MSW | Playwright |
| Confiança no comportamento real | Baixa, isolada | Alta para ligações internas | Máxima, mas com menos casos |
| Custo de manutenção | Baixo | Médio | Alto |
| Melhor uso | Regras de negócio puras | Fluxos de tela e tratamento de erro | Fluxos críticos de negócio |
A tabela ajuda, mas a decisão real sai de uma pergunta por comportamento: se esse bug chegar à produção, quanto custa, e qual é o teste mais barato que o pegaria?
17. Antipadrões que aparecem em quase todo projeto
O primeiro é o E2E para tudo. A suíte fica lenta, o CI demora e ninguém roda localmente. O segundo é o unitário que mocka tudo, que passa mesmo quando o sistema está quebrado. O terceiro é a meta de cobertura. Cobertura de linhas mede quais linhas foram executadas, não quais comportamentos foram verificados. Uma suíte com 90% de cobertura e asserções fracas pode deixar passar bugs óbvios. Use cobertura como sinal para achar áreas sem teste, não como objetivo a ser batido.
O quarto é testar o que o framework já garante, como verificar se o React renderiza um elemento que você passou como prop. O quinto é ignorar a acessibilidade e usar data-testid em tudo. O data-testid é útil como último recurso, mas, quando é o primeiro, a suíte perde a chance de achar problemas reais de leitura de tela.
Parte 6: como criar um teste do zero
18. Um roteiro para uma feature
Suponha uma tela de carrinho que calcula o total com cupom de desconto. Um roteiro que funciona:
- Liste os comportamentos observáveis em linguagem de usuário: “aplicar um cupom válido atualiza o total”, “cupom inválido mostra erro”, “remover item recalcula o total”.
- Para cada comportamento, escolha o nível. A regra de cálculo do total vai para unitário, porque é pura. O cupom inválido e a atualização da tela vão para integração, porque envolvem a API e o componente. O fluxo de compra completo com cupom vai para E2E, e só um caso feliz.
- Escreva o teste antes da implementação quando puder. Ele deve falhar pelo motivo certo, por exemplo “o total não mudou”, e não por erro de importação.
- Implemente o mínimo para o teste passar.
- Refatore sem mudar o comportamento e rode a suíte de novo.
Esse ciclo de falhar, implementar e refatorar é o TDD. Ele não é obrigatório, mas força a pensar no comportamento antes do código, que é justamente o hábito que deixa a suíte útil.
Parte 7: testes no pipeline
19. Onde cada nível roda
Teste unitário e de integração rodam a cada commit e a cada pull request, porque são rápidos e dão feedback enquanto o desenvolvedor ainda está no contexto. O E2E roda em pull request para os fluxos críticos, e pode ser dividido em partes (sharding) e executado em paralelo para caber no tempo do pipeline. Um E2E completo antes de cada deploy é uma escolha comum para ambientes de produção.
Uma prática que ajuda é publicar o relatório do Playwright e a cobertura como artefatos do CI. Quando um teste falha, quem olhou a falha consegue ver a captura de tela e o trace sem precisar reproduzir o ambiente.
Exercícios
- Um componente de lista mostra “Nenhum produto encontrado” quando a API devolve uma lista vazia. Em qual nível você testaria isso, e por quê?
- Uma função
calcularFrete(peso, distancia)tem três faixas de preço. Quantos casos de teste você escreveria, e quais são os limites de cada faixa? - Um fluxo de recuperação de senha envia um e-mail real. Como você testaria esse fluxo sem enviar e-mails de verdade, e em qual nível?
Respostas e comentários
- Integração. O comportamento depende do componente, da resposta da API e da renderização condicional. Um MSW que devolve lista vazia cobre a ligação inteira, e um teste unitário só verificaria um ramo do código.
- Pelo menos um caso por faixa, mais os limites entre elas. Se as faixas são até 5 kg, de 5 a 20 kg e acima de 20 kg, teste 5 kg, 5,01 kg, 20 kg e 20,01 kg, além de um valor típico em cada faixa. Os limites são onde a maioria dos erros de comparação acontece (
<contra<=). - Separe as duas coisas. No teste de integração, use o MSW para interceptar a chamada HTTP ao serviço de e-mail e verifique que a requisição saiu com os dados certos. No E2E, o Playwright permite interceptar requisições com
page.route, e um único caso basta para confirmar que o fluxo aparece para o usuário. Assim o envio real de e-mail nunca acontece no teste.
Erros comuns
- Testar a implementação em vez do comportamento, o que torna a suíte frágil a qualquer refatoração.
- Usar
waitForcom tempo fixo, que cria testes lentos e instáveis. - Mockar a própria lógica testada, o que faz o teste passar mesmo com bug.
- Deixar dados vazarem entre testes, por estado global ou banco compartilhado.
- Escrever E2E para validações de campo que um teste de integração cobre melhor.
- Perseguir cobertura de 100% com asserções fracas.
Fontes
- Testing Library: documentação do React Testing Library e dos princípios de consulta. https://testing-library.com/
- Vitest: documentação oficial do executor de testes. https://vitest.dev/
- MSW (Mock Service Worker): documentação de interceptação de requisições. https://mswjs.io/
- Playwright: documentação oficial de testes E2E. https://playwright.dev/
- Kent C. Dodds: artigos sobre o formato de troféu de testes, em kentcdodds.com.