Teste unitário vs teste de integração: diferença na prática
A diferença entre teste unitário e de integração com exemplos de API e banco de dados: o que cada um pega, quanto custa e quando usar cada um no seu projeto.
O repositório de usuários estava coberto. Noventa e um por cento de coverage, todos os testes verdes, deploy no ar. Aí o time reclamou que o cadastro estava salvando usuário sem enviar o e-mail de confirmação. O bug estava na integração entre o service de usuário e o service de e-mail — dois componentes unitariamente perfeitos que, juntos, nunca haviam sido testados.
A diferença em uma frase: o teste unitário verifica uma unidade de código isolada, com as dependências substituídas por dublês; o teste de integração verifica que duas ou mais partes reais funcionam juntas — o repositório com o banco de dados, o endpoint da API com o service e o banco. O unitário pega erro de lógica; o de integração pega erro de encaixe. O resto do post mostra o que cada um pega na prática, com exemplos de API e banco de dados.
A diferença em uma tabela
| Teste unitário | Teste de integração | |
|---|---|---|
| Escopo | Uma função, método ou classe | Duas ou mais partes reais juntas (código + banco, API + service + banco, consumidor + fila) |
| Dependências | Substituídas por mocks, stubs ou fakes | Reais, ou o mais próximo possível (banco num container, serviço externo simulado na rede) |
| Velocidade | Milissegundos por teste | De dezenas de milissegundos a segundos por teste, mais o setup |
| O que pega | Erro de lógica: cálculo, regra condicional, caso de borda | Erro de encaixe: query errada, mapeamento, migração, contrato entre camadas, transação |
| O que não pega | Qualquer problema na fronteira com o que foi mockado | Combinações finas de lógica (caro demais testar todas por ali) |
| Quando falha | Aponta para uma função | Aponta para um fluxo; exige investigar qual parte |
| Ferramentas comuns | pytest, JUnit, Vitest, go test |
As mesmas, mais Testcontainers, banco de teste, cliente HTTP de teste |
O que cada tipo de teste enxerga
Um teste unitário isola uma unidade e verifica seu comportamento com entradas controladas. Tudo que está fora dela é substituído por dublês. O teste é rápido, determinístico e cirúrgico — e cego para tudo que o dublê esconde.
Um teste de integração verifica que componentes funcionam corretamente quando acoplados. A diferença não é de granularidade — é de o que você quer garantir. O unitário garante que a lógica interna de uma função está correta. O de integração garante que a função se comporta corretamente dentro do sistema.
O bug do e-mail da abertura não seria pego por nenhum teste unitário, por melhor que fosse. O service de e-mail tinha seus mocks, o de usuário tinha os dele. Ninguém testou o ponto de contato — a interface real entre os dois.
Custo e velocidade
Testes unitários: escrita rápida e sem infraestrutura; execução em milissegundos, milhares rodam em segundos; manutenção proporcional ao número de mocks — quanto mais você mocka, mais o teste quebra quando a implementação muda, mesmo sem mudança de comportamento.
Testes de integração: escrita mais lenta, porque exige setup (banco, containers, dados de teste); execução mais lenta, e suítes grandes levam minutos; manutenção mais estável que a do unitário quando um refactor não altera o comportamento externo.
A matemática parece favorecer o unitário. Mas há uma variável escondida: o custo de um bug de integração em produção é muito maior que o custo do teste que o teria pego. Times que só escrevem unitários tendem a ter coverage alto e deploys que mesmo assim quebram fluxos reais. Times que só escrevem integração têm suítes lentas e feedback lento no desenvolvimento.
E a pirâmide de testes?
A pirâmide de Mike Cohn põe muitos unitários na base, menos testes de integração no meio e poucos e2e no topo. Formas mais recentes — o troféu de Kent C. Dodds no front-end, o favo de mel do Spotify para microsserviços — dão mais peso à integração. Expliquei o modelo, o anti-padrão do cone de sorvete e essas alternativas em pirâmide de testes: o que é e camadas; aqui fica só a pergunta que decide o equilíbrio no seu projeto: de onde vêm os bugs de produção do seu sistema?
Quando usar teste unitário
Lógica de domínio com múltiplos caminhos é o candidato natural. Se você tem uma função de desconto com oito combinações de tipo de cliente e valor de compra, quer testar as oito em milissegundos — não subindo um banco a cada vez.
O post Testes unitários: o que testar cobre esse território em detalhe. A conclusão de lá: o unitário tem valor máximo quando a função é pura, tem vários caminhos e um comportamento estável o suficiente para o teste não quebrar a cada refactor. Outros bons candidatos:
- Parsers e transformações de dados com casos de borda
- Validações com regras complexas
- Funções de formatação (datas, moeda, documentos) com entradas não óbvias
- Regras de autorização com combinações de permissão
O que não vale testar com unitário: qualquer coisa cuja única "lógica" é chamar outra coisa. Controller que repassa ao service, repositório que só chama o ORM, adapter que só converte formato — sem lógica, não há o que testar isoladamente.
Quando usar teste de integração
Repositório e banco de dados
O repositório é o caso mais óbvio. Mockar o banco num teste de repositório derrota o propósito: você precisa saber se a query está correta, se o mapeamento está certo, se a constraint existe. Um teste de repositório que não bate no banco é, literalmente, um teste de mock.
# Isso não testa o repositório — testa se você sabe configurar um mock
def test_buscar_usuario_mock(mock_session):
repo = UsuarioRepository(session=mock_session)
mock_session.execute.return_value = MockResult(usuario_fake)
resultado = repo.buscar_por_email("test@example.com")
assert resultado.email == "test@example.com"
# Isso testa o repositório
def test_buscar_usuario_banco(db_session):
repo = UsuarioRepository(session=db_session)
db_session.add(Usuario(email="test@example.com", nome="Teste"))
db_session.commit()
resultado = repo.buscar_por_email("test@example.com")
assert resultado.email == "test@example.com"
O segundo pega problemas reais: query errada, campo mapeado com outro nome, constraint violada, migração não aplicada. O primeiro não pega nada disso.
API + banco de dados
Esse é o caso que mais gera dúvida: o endpoint da API deve ser testado com o banco mockado ou real? Para o fluxo principal, real. O teste chama o endpoint por um cliente HTTP de teste, deixa o service e o repositório trabalharem de verdade, e confere o que ficou gravado no banco. Rodei esta versão com FastAPI, SQLAlchemy 2 e pytest contra um PostgreSQL 18 — dois testes, 0,62 s:
@pytest.fixture
def client(db_session):
# troca só a sessão do banco pela sessão de teste; o resto é código real
app.dependency_overrides[get_session] = lambda: db_session
yield TestClient(app)
app.dependency_overrides.clear()
def test_criar_pedido_grava_total_no_banco(client, db_session):
resp = client.post("/pedidos", json={"cliente_id": 1, "itens": [{"sku": "A1", "qtd": 2}]})
assert resp.status_code == 201
pedido = db_session.execute(
text("SELECT status, total FROM pedidos WHERE id = :id"), {"id": resp.json()["id"]}
).one()
assert pedido.status == "pendente"
assert str(pedido.total) == "39.80" # 2 x 19.90, calculado com o preço que está no banco
Esse teste pega o que nenhum unitário do handler pegaria: validação do payload, serialização, a query de preço, o INSERT ... RETURNING, o tipo NUMERIC voltando como esperado. A fixture db_session limpa as tabelas e insere o produto A1 antes de cada teste — dados conhecidos, sem estado compartilhado. Para mais padrões nessa linha (contratos de resposta, autenticação, dados de teste), veja como testar APIs de forma automatizada.
Outros casos
- Processamento de filas: verificar que a mensagem publicada é consumida e processada, com o broker real num container.
- Serviços externos: quando viável, a API real em sandbox. Quando não, um servidor simulado na rede (WireMock, Prism) que se comporta como o serviço — não um mock de objeto dentro do código.
- Pipelines de transformação: quando dados passam por várias etapas, testar a pipeline inteira costuma valer mais que cada passo isolado.
O erro mais caro: mockar o que deveria ser integrado
Existe um padrão que parece prudente e é uma armadilha: mockar o banco nos testes de repositório, mockar o service de e-mail nos testes de notificação, mockar a API externa nos testes de pagamento — e nunca ter um teste que verifique a integração real.
O resultado é uma suíte com 90%+ de coverage em que cada componente está "testado" e o sistema como um todo nunca foi exercitado. Os números são bons; a confiança é falsa. O post sobre mocks, stubs e fakes separa quando cada tipo de dublê ajuda e quando ele esconde exatamente o bug que importa.
A heurística que uso: se dois componentes são separados no código mas acoplados no comportamento de produção, o teste relevante é de integração. Interfaces entre camadas, callbacks, eventos — é ali que os bugs nascem, e é exatamente o que o unitário pula por design.
Infraestrutura para testes de integração
O maior argumento contra integração é o custo de setup. Ele é real, mas resolvível. O Testcontainers sobe um container de banco (Postgres, MySQL, Redis, o que for) no início da suíte e o descarta no final — sem estado compartilhado entre execuções e sem depender de banco configurado na máquina de cada um.
# testcontainers-python 4.x (em versões antigas: from testcontainers.postgres import ...)
from testcontainers.community.postgres import PostgresContainer
@pytest.fixture(scope="session")
def database_url():
with PostgresContainer("postgres:18", driver="psycopg") as pg:
yield pg.get_connection_url() # postgresql+psycopg://...
Há versões para Go (testcontainers-go), Java (a biblioteca original), Node e .NET. Aqui, com a imagem já baixada, o postgres:18 ficou pronto em 2 a 3 segundos; o primeiro pull custa bem mais. Com scope="session", esse custo é pago uma vez por suíte.
Para APIs de terceiros, uso testes de contrato: define o que a API recebe e devolve, testa contra um simulador que cumpre esse contrato e mantém um teste separado, mais raro, que valida o contrato contra a API real. O post sobre testes de contrato entre serviços detalha o padrão.
Perguntas frequentes
Qual a diferença prática entre teste de integração e teste E2E?
O teste de integração verifica partes do sistema funcionando juntas, geralmente sem interface de usuário, via código ou HTTP. O E2E dirige o sistema completo pela interface — normalmente um navegador com Playwright ou Cypress. Integração é mais rápida e mais fácil de depurar; E2E é mais próximo do usuário real, mas bem mais lento e frágil. Use E2E para os fluxos críticos — o post sobre quando testes e2e compensam ajuda a escolher quais.
Posso usar SQLite em memória no lugar do banco de produção nos testes de integração?
Só se o código usar SQL portável. SQLite difere do Postgres em tipos (tipagem flexível por padrão), JSON, upserts e concorrência; um teste que passa no SQLite pode falhar no Postgres por diferença de dialeto. Se a produção é Postgres, teste com Postgres — o Testcontainers torna isso simples o bastante. O SQLite brilha em outros cenários, que explico em quando usar SQLite.
Qual a proporção certa entre unitário, integração e e2e?
Não existe proporção universal; percentuais fixos como 70/20/10 são convenção, não regra. Decida pelo risco: onde a lógica de domínio é densa (cálculos, regras condicionais, validações), unitários pagam mais e a pirâmide clássica descreve bem a suíte; onde o comportamento está nas fronteiras (APIs CRUD, banco, filas, microsserviços), integração paga mais e a suíte tende ao favo de mel — é comum um back-end de API ter tantos testes de integração quanto unitários, ou mais. Nos dois casos, e2e ficam poucos e reservados aos fluxos críticos.
Mocks são ruins em testes de integração?
Mocks têm lugar — para serviços externos que você não controla (gateway de pagamento, SMS, e-mail transacional), de preferência simulados na fronteira de rede. O problema é mockar componentes internos que você poderia testar de verdade. Se você está mockando o próprio service dentro de um teste de integração, provavelmente está testando a camada errada.
Unitário e integração não concorrem, se completam
A tensão entre os dois tipos é falsa. O unitário resolve bem problemas de lógica interna; o de integração resolve problemas de acoplamento e contrato entre componentes. Um sistema saudável usa os dois onde cada um tem vantagem. O que não funciona é usar um como substituto do outro por conveniência — mockar tudo para ter testes rápidos, ou escrever só E2E porque "é mais realista". Os bugs de produção mais caros que vi estavam todos em pontos de integração cobertos por mocks. Coverage de código não é coverage de comportamento real.
Uma coisa que barateia muito os testes de integração é ter dados de teste prontos. Para os seeds e payloads de exemplo — clientes, pedidos, produtos com campos variados — gero o JSON no Gerador de Dados Mock JSON e versiono junto dos testes.
- 01 Base64 online seguro: ferramentas que não enviam seus dados Como saber se uma ferramenta de Base64 online processa no navegador ou no servidor, quais opções gratuitas usar para tokens e arquivos e quando evitar sites.
- 02 Como decodificar JWT em JavaScript sem biblioteca Leia header e payload de um JWT com atob e TextDecoder, sem dependências. Veja por que decodificar no navegador é seguro e por que validar lá não é.