Mocks e stubs: simulando dependências externas em testes
1. Fundamentos: O Problema das Dependências Externas
1.1. Por que dependências externas tornam testes lentos e frágeis
Dependências externas — APIs REST, bancos de dados, filas de mensagens, serviços de e-mail — são o calcanhar de Aquiles dos testes automatizados. Uma chamada HTTP real pode levar centenas de milissegundos; uma consulta ao banco de dados, dezenas. Multiplique por centenas de testes e seu pipeline de CI/CD se arrasta por minutos. Pior: se o serviço externo estiver fora do ar, seus testes quebram mesmo sem você ter alterado uma linha de código. Testes frágeis minam a confiança da equipe.
1.2. O conceito de isolamento em testes
Testes de unidade exigem isolamento total: a classe sob teste deve ser executada como uma ilha, sem tocar em infraestrutura real. Já testes de integração verificam a comunicação entre componentes, incluindo sistemas externos controlados (como bancos em memória). Mocks e stubs são as ferramentas que criam esse isolamento artificial, substituindo dependências reais por versões controladas.
1.3. Diferença prática entre stubs, mocks, fakes e dummies
- Dummy: objeto passado como argumento, mas nunca usado (ex.:
nullou objeto vazio). - Fake: implementação funcional simplificada (ex.: banco de dados em memória).
- Stub: fornece respostas prontas para chamadas feitas durante o teste.
- Mock: verifica se métodos específicos foram chamados com os argumentos esperados.
A linha entre stub e mock é sutil: stubs focam em estado (o que retornam), mocks focam em comportamento (como foram chamados).
2. Stubs: Simulando Respostas Fixas
2.1. Como criar stubs
Stubs são ideais quando você precisa que uma dependência retorne dados previsíveis. Não verificam interações — apenas fornecem respostas.
2.2. Exemplo: stub de repositório
// Stub de repositório de usuários
class UserRepositoryStub implements UserRepository {
private final List<User> users;
UserRepositoryStub(List<User> users) {
this.users = users;
}
@Override
public User findById(String id) {
return users.stream()
.filter(u -> u.getId().equals(id))
.findFirst()
.orElse(null);
}
}
// Teste usando o stub
@Test
void shouldReturnUserWhenExists() {
UserRepositoryStub stub = new UserRepositoryStub(
List.of(new User("1", "Alice"))
);
UserService service = new UserService(stub);
User result = service.getUser("1");
assertEquals("Alice", result.getName());
}
2.3. Quando usar stubs
Use stubs quando o teste precisa apenas dos dados retornados, não de verificar como a dependência foi chamada. Evite stubs para lógicas complexas — nesse caso, prefira fakes ou mocks.
3. Mocks: Verificando Interações e Comportamento
3.1. A diferença essencial
Mocks registram expectativas: "o método X deve ser chamado com argumentos Y". Se a chamada não ocorrer, o teste falha. Isso é crucial para verificar efeitos colaterais, como envio de e-mails ou registro em logs.
3.2. Exemplo: mock de serviço de e-mail
// Mock de serviço de notificação
EmailService mockEmail = mock(EmailService.class);
NotificationService notification = new NotificationService(mockEmail);
notification.sendWelcomeEmail("user@example.com");
verify(mockEmail).send(
"user@example.com",
"Bem-vindo!",
"Seu cadastro foi concluído."
);
3.3. Armadilhas comuns
Mocks excessivos criam testes frágeis — qualquer mudança na implementação quebre o teste, mesmo que o comportamento externo permaneça correto. A regra de ouro: mocke apenas o que você possui (sua própria interface), nunca bibliotecas terceiras.
4. Ferramentas e Frameworks Populares
4.1. Visão geral
- Mockito (Java): framework maduro com anotações
@Mocke@InjectMocks. - unittest.mock (Python):
patcheMagicMockpara substituir objetos dinamicamente. - Jest mock (JavaScript):
jest.fn()ejest.mock()para módulos inteiros.
4.2. Exemplo: mock com retorno condicional
# Python com unittest.mock
from unittest.mock import Mock
api_client = Mock()
api_client.fetch_data.side_effect = [
{"status": "ok", "data": [1, 2, 3]},
{"status": "error", "message": "timeout"}
]
# Primeira chamada
result1 = api_client.fetch_data() # retorna {"status": "ok", ...}
# Segunda chamada
result2 = api_client.fetch_data() # retorna {"status": "error", ...}
4.3. Spy vs. mock
Spies envolvem objetos reais e permitem verificar chamadas sem substituir a implementação. Use spies quando quiser testar o comportamento real, mas ainda monitorar interações.
5. Estratégias Avançadas de Simulação
5.1. WireMock e Testcontainers
Para simular serviços HTTP completos, WireMock cria servidores stub que respondem com headers, delays e erros específicos. Testcontainers sobe bancos de dados reais em containers Docker para testes de integração.
5.2. Simulação de falhas
Testar cenários de erro é essencial:
// Mock de serviço que simula timeout
when(apiClient.call()).thenThrow(new TimeoutException("Serviço indisponível"));
// Teste verifica tratamento de erro
assertThrows(GatewayException.class, () -> service.process());
5.3. Dependências encadeadas
Mock de mock (ex.: servicoA.getRepositorio().buscar()) é um anti-padrão. Prefira injeção de dependência direta: passe o repositório mockado diretamente, sem encadeamento.
6. Boas Práticas e Anti-Padrões
6.1. Não mocke o que você não possui
Bibliotecas terceiras (ex.: AWS SDK, Stripe) devem ser encapsuladas atrás de suas próprias interfaces. Mocke sua interface, não a biblioteca.
6.2. Evite mocks de objetos de valor
Entidades simples (User, Product) não precisam ser mockadas — use objetos reais com dados de teste.
6.3. Factory methods para mocks limpos
// Factory method para criar mocks configurados
private EmailService createEmailMock(boolean shouldFail) {
EmailService mock = mock(EmailService.class);
if (shouldFail) {
doThrow(new SendException()).when(mock).send(any());
}
return mock;
}
7. Mocks no Contexto de TDD e Testes de Integração
7.1. Mocks no ciclo TDD
No TDD, mocks permitem escrever o teste antes da implementação: defina o comportamento esperado, implemente até o teste passar, depois refatore.
7.2. Transição de mocks para testes reais
Comece com mocks para validar a lógica. Gradualmente, adicione testes de integração com containers reais para verificar o comportamento completo.
7.3. Exemplo comparativo
// Teste unitário com mock
@Test
void testWithMock() {
Database mockDb = mock(Database.class);
when(mockDb.find("1")).thenReturn(new User("1", "Alice"));
User result = new UserService(mockDb).getUser("1");
assertEquals("Alice", result.getName());
}
// Teste de integração com container
@Test
void testWithRealDb() {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")) {
DataSource ds = createDataSource(postgres);
UserService service = new UserService(new RealUserRepository(ds));
service.createUser(new User("1", "Alice"));
User result = service.getUser("1");
assertEquals("Alice", result.getName());
}
}
8. Conclusão e Checklist Final
8.1. Resumo das diferenças
- Stub: você pergunta "o que retornar?" — foco em estado.
- Mock: você pergunta "foi chamado?" — foco em comportamento.
- Fake: implementação simplificada, mas funcional.
8.2. Checklist para decidir
| Cenário | Ferramenta |
|---|---|
| Preciso de dados fixos | Stub |
| Preciso verificar chamadas | Mock |
| Preciso de lógica real simplificada | Fake |
| Teste de integração completo | Container real |
8.3. Referência rápida
Mocks e stubs são aliados poderosos quando usados com moderação. Lembre-se: o objetivo não é testar a implementação, mas o comportamento observável. Use stubs para dados, mocks para interações, e sempre prefira testes reais quando o custo for aceitável.
Referências
- Martin Fowler - Mocks Aren't Stubs — Artigo clássico que define e diferencia os conceitos de mocks, stubs, fakes e dummies com exemplos detalhados.
- Mockito Official Documentation — Documentação oficial do framework Mockito para Java, com guias de uso, anotações e boas práticas.
- Python unittest.mock — Mock object library — Documentação oficial da biblioteca de mocks do Python, incluindo patch, MagicMock e side_effect.
- WireMock - Simulating HTTP Services — Ferramenta para criar stubs de serviços HTTP, ideal para testar integrações com APIs externas.
- Testcontainers - Integration Testing with Real Databases — Biblioteca que permite subir containers Docker em testes de integração, substituindo mocks por dependências reais controladas.