Como mock, stub e fake são diferentes
1. Visão geral
Ao escrever testes unitários, você frequentemente precisa substituir dependências reais como banco de dados, gateway de pagamento ou serviço de email por um objeto “substituto”. Esses objetos são chamados coletivamente de test double. Muitas pessoas chamam tudo de “mock”, mas mock, stub e fake servem três propósitos diferentes.
Este artigo o ajuda a diferenciar esses três tipos através de um exemplo único, saber quando usar cada um e evitar erros que tornam o teste frágil ou sem significado.
2. Exemplo usado ao longo do artigo
2.1. Dependências
Neste artigo, usamos JUnit 5, AssertJ e Mockito. Se você estiver usando Spring Boot 3.x, spring-boot-starter-test já inclui os três. Caso contrário, adicione as seguintes dependências:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.14.2</version>
<scope>test</scope>
</dependency>
</dependencies>
2.2. Código a testar
Temos um OrderService simples. Este serviço cobra o cliente, salva o pedido e depois envia um email de confirmação:
public record Order(String id, String customerEmail, BigDecimal amount) {}
public interface PaymentGateway {
boolean charge(String customerEmail, BigDecimal amount);
}
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
}
public interface EmailSender {
void send(String to, String subject);
}
public class OrderService {
private final PaymentGateway paymentGateway;
private final OrderRepository orderRepository;
private final EmailSender emailSender;
public OrderService(PaymentGateway paymentGateway,
OrderRepository orderRepository,
EmailSender emailSender) {
this.paymentGateway = paymentGateway;
this.orderRepository = orderRepository;
this.emailSender = emailSender;
}
public boolean placeOrder(Order order) {
if (!paymentGateway.charge(order.customerEmail(), order.amount())) {
return false;
}
orderRepository.save(order);
emailSender.send(order.customerEmail(), "Pedido " + order.id() + " foi confirmado");
return true;
}
}
OrderService tem três dependências. Cada dependência é adequada para um tipo diferente de test double:
PaymentGatewayfornece dados de entrada (resultado do pagamento), então é adequado para stub.EmailSenderé um efeito colateral que precisa ser verificado (o email foi enviado?), então é adequado para mock.OrderRepositorytem comportamento de persistência real, então é adequado para fake.
3. Stub: retorna uma resposta pré-construída
Stub é um objeto que retorna um valor fixo para as chamadas. Stub não se importa quantas vezes é chamado. Você também não faz assert sobre o stub. O único propósito do stub é colocar o código sob teste no branch de lógica correto que você quer verificar.
3.1. Stub escrito manualmente
A forma mais simples é implementar a interface você mesmo:
class DecliningPaymentGateway implements PaymentGateway {
@Override
public boolean charge(String customerEmail, BigDecimal amount) {
return false;
}
}
Esta classe sempre rejeita o pagamento. Usamos para verificar o branch “pagamento rejeitado”:
class OrderServiceStubTest {
@Test
void givenDeclinedPayment_whenPlaceOrder_thenReturnFalse() {
OrderService service = new OrderService(
new DecliningPaymentGateway(),
mock(OrderRepository.class),
mock(EmailSender.class));
boolean result = service.placeOrder(
new Order("ORD-1", "an@example.com", new BigDecimal("150000")));
assertThat(result).isFalse();
}
}
Neste teste:
DecliningPaymentGatewayé um stub. ForçaplaceOrder()a entrar no branchreturn false.- A asserção está no resultado retornado pelo serviço, não no stub.
mock(OrderRepository.class)emock(EmailSender.class)aqui apenas fazem papel de dummy. São objetos passados apenas para satisfazer os parâmetros do constructor, não são usados.
3.2. Stub com Mockito
Escrever uma classe para cada cenário rapidamente se torna verboso. Mockito permite criar stubs diretamente no teste:
@Test
void givenApprovedPayment_whenPlaceOrder_thenReturnTrue() {
PaymentGateway paymentGateway = mock(PaymentGateway.class);
when(paymentGateway.charge(anyString(), any(BigDecimal.class))).thenReturn(true);
OrderService service = new OrderService(
paymentGateway, mock(OrderRepository.class), mock(EmailSender.class));
assertThat(service.placeOrder(
new Order("ORD-2", "binh@example.com", new BigDecimal("99000"))))
.isTrue();
}
Embora criado com a função mock(), paymentGateway aqui ainda é um stub em termos de função: apenas configuramos o valor de retorno com when(...).thenReturn(...) e nunca o verify. O nome da função da biblioteca não determina o tipo de test double. Como você usa o objeto é o que realmente importa.
4. Mock: verificação de interações
Mock é um objeto que registra as chamadas para ele, para que você possa depois verificar se o código chamou o método correto, com os parâmetros corretos, o número correto de vezes. Diferente de stub, a asserção fica sobre o próprio mock.
Mock é útil quando o comportamento a verificar é um efeito colateral que não retorna nada, como enviar email, publicar um evento ou registrar auditoria.
@ExtendWith(MockitoExtension.class)
class OrderServiceMockTest {
@Mock
PaymentGateway paymentGateway;
@Mock
OrderRepository orderRepository;
@Mock
EmailSender emailSender;
@InjectMocks
OrderService orderService;
@Test
void givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail() {
when(paymentGateway.charge("an@example.com", new BigDecimal("150000")))
.thenReturn(true);
orderService.placeOrder(new Order("ORD-1", "an@example.com", new BigDecimal("150000")));
verify(emailSender).send("an@example.com", "Pedido ORD-1 foi confirmado");
}
@Test
void givenDeclinedPayment_whenPlaceOrder_thenNoEmailSent() {
when(paymentGateway.charge(anyString(), any(BigDecimal.class))).thenReturn(false);
orderService.placeOrder(new Order("ORD-1", "an@example.com", new BigDecimal("150000")));
verifyNoInteractions(emailSender, orderRepository);
}
}
Analisando cada parte:
@ExtendWith(MockitoExtension.class)ativa Mockito para JUnit 5. Os campos@Mocksão inicializados antes de cada teste.@InjectMockscriaOrderServicee injeta os três objetos@Mockatravés do constructor.paymentGatewayainda é um stub, enquantoemailSenderé o verdadeiro mock porque fazemosverifynele.verify(emailSender).send(...)verifica sesend()foi chamado exatamente uma vez com os dois parâmetros corretos.verifyNoInteractions(...)afirma que quando o pagamento falha, o sistema não salva o pedido e não envia email.
Se você acidentalmente deletar a linha emailSender.send(...) em OrderService, o primeiro teste falhará com uma mensagem similar:
Wanted but not invoked:
emailSender.send(
"an@example.com",
"Pedido ORD-1 foi confirmado"
);
-> at OrderServiceMockTest.givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail
Actually, there were zero interactions with this mock.
Nota: Você não deve nem stub nem verify a mesma chamada, como
when(paymentGateway.charge(...))e depoisverify(paymentGateway).charge(...). Se o stub não for chamado, o serviço não retornoutruee o teste falhará em outro lugar. O comandoverifyneste caso é redundante e apenas acopla mais o teste à implementação.
5. Fake: implementação real mas simplificada
Fake é uma implementação que funciona de verdade mas usa atalhos inadequados para produção. Um exemplo típico é um repositório que armazena dados em um HashMap em vez de usar um banco de dados:
public class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> storage = new HashMap<>();
@Override
public void save(Order order) {
storage.put(order.id(), order);
}
@Override
public Optional<Order> findById(String id) {
return Optional.ofNullable(storage.get(id));
}
}
Diferente de stub, fake tem lógica e estado. Chamar save() e depois findById() retornará o pedido que foi salvo. Assim, você verifica o estado final em vez de verificar chamadas:
class OrderServiceFakeTest {
private final InMemoryOrderRepository orderRepository = new InMemoryOrderRepository();
private final OrderService orderService = new OrderService(
(email, amount) -> true, // stub em forma de lambda
orderRepository, // fake
(to, subject) -> { }); // dummy em forma de lambda
@Test
void givenApprovedPayment_whenPlaceOrder_thenOrderIsPersisted() {
Order order = new Order("ORD-3", "chi@example.com", new BigDecimal("250000"));
orderService.placeOrder(order);
assertThat(orderRepository.findById("ORD-3")).contains(order);
}
}
Alguns pontos a notar:
PaymentGatewayeEmailSendertêm apenas um método abstrato, então criamos stub e dummy usando lambda sem precisar de Mockito.- O teste não sabe quantas vezes
OrderServicechamasave()ou em que ordem. O teste apenas se importa se após fazer o pedido, o pedido está armazenado. - Se mais tarde você refatorar
OrderServicepara usar um método diferente, comosaveAll(), o teste usando fake ainda funcionará desde que o resultado final esteja correto. O teste usandoverify(orderRepository).save(order)quebrará.
Nota: Fake é código, então fake também pode ter bugs. Se fake se comportar diferente da implementação real (por exemplo, não lançar exceção em caso de chave duplicada), o teste pode passar enquanto em produção falha. Para fakes importantes, escreva um conjunto de contract tests compartilhados que rodem tanto no fake quanto na implementação real (por exemplo, a versão JPA rodando com Testcontainers).
6. Comparação e como escolher
A tabela abaixo resume as diferenças:
| Critério | Stub | Mock | Fake |
|---|---|---|---|
| Propósito | Fornecer dados de entrada | Verificar interações | Substituir comportamento real de forma mais leve |
| Tem lógica? | Não, retorna valor fixo | Não, apenas registra chamadas | Sim, funciona como a versão real |
| Onde fazer assert | No resultado do código sob teste | No próprio mock (verify) |
No estado do fake |
| Tipo de teste | Baseado em estado | Baseado em interações | Baseado em estado |
| Resistência a refatoração | Alta | Mais baixa | Alta |
Além dos três tipos acima, você ainda encontrará dois outros:
- Dummy: objeto passado apenas para satisfazer parâmetros, nunca realmente usado.
- Spy: envolve um objeto real, ainda executa a lógica real mas registra chamadas para
verify. No Mockito, você cria um spy comspy(...).
Uma regra simples de seleção:
- Dependência que retorna dados para seu código, use stub.
- Dependência que é um efeito colateral externo (email, message queue, API de terceiros) e não há forma de observar o estado, use mock.
- Dependência que tem estado e é usada em muitos testes (repositório, cache, relógio), considere escrever um fake uma vez e reutilizá-lo em vários lugares.
Em geral, você deve priorizar verificações de estado (stub, fake) sobre verificações de interação (mock). Testes baseados em estado descrevem o resultado, enquanto testes baseados em interação descrevem como fazer, e como fazer muda com mais frequência.
7. Erros comuns
Mockar tudo. Se um teste tem cinco comandos when e quatro comandos verify, esse teste está copiando a implementação em vez de verificar o comportamento. Basta mudar a ordem de duas linhas de código e o teste ficará vermelho mesmo que o resultado ainda esteja correto.
Mockar value objects. Não mocke Order, BigDecimal ou LocalDate. Crie instâncias reais. Mockito 5 pode mockar classes final e records por padrão, mas só porque você pode fazer algo não significa que deva.
Mockar tipos que você não possui. Mockar diretamente RestTemplate ou SDK de um fornecedor faz o teste assumir comportamentos incorretos sobre a biblioteca. Envolva a biblioteca atrás de uma interface sua (como PaymentGateway) e depois mocke a interface. A implementação real é verificada com testes de integração.
Stubs desnecessários. Com MockitoExtension, Mockito usa strict stubs por padrão. Se você criar um stub para uma chamada que o código nunca executa, o teste falhará:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Stubbings desnecessários detectados.
Código de teste limpo e mantível requer zero código desnecessário.
Não desative este aviso apressadamente com lenient(). Esta mensagem geralmente indica que o teste não está entrando no branch que você pensa, ou que a configuração está desatualizada.
Usar fake no lugar de integration test. InMemoryOrderRepository não consegue verificar SQL, constraints ou transações. Fake torna testes unitários rápidos, mas você ainda precisa de alguns testes de integração com banco de dados real.
8. Conclusão
Stub, mock e fake são todos test doubles mas respondem a três perguntas diferentes. Stub responde “o que a dependência retorna?”. Mock responde “o código chama a dependência corretamente?”. Fake responde “se houvesse uma versão que funciona de verdade mas de forma mais leve, qual seria o resultado final?”.
Escolha o tipo de test double baseado no papel da dependência, não na ferramenta: um objeto criado com mock() pode perfeitamente ser apenas um stub. Priorize verificações de estado e use verify apenas para efeitos colaterais que realmente precisam ser verificados. Seus testes serão mais curtos, mais fáceis de ler e quebrarão menos durante refatoração.