How Mock, Stub and Fake Differ
1. Overview
When writing unit tests, you often need to replace real dependencies like databases, payment gateways, or email services with a stand-in object. These objects are collectively called test doubles. Many people call them all “mocks,” but mock, stub, and fake serve three different purposes.
This article helps you distinguish between these three types through a single example, understand when to use each one, and avoid mistakes that make tests brittle or meaningless.
2. Example Used Throughout the Article
2.1. Dependencies
In this article, we’ll use JUnit 5, AssertJ, and Mockito. If you’re using Spring Boot 3.x, spring-boot-starter-test includes all three. Otherwise, add the following dependencies:
<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. Code Under Test
We have a simple OrderService. This service charges the customer, saves the order, and sends a confirmation email:
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(), "Order " + order.id() + " has been confirmed");
return true;
}
}
OrderService has three dependencies. Each dependency fits a different test double type:
PaymentGatewayprovides input data (payment result), so it suits a stub.EmailSenderis an external side effect that needs verification (was the email sent?), so it suits a mock.OrderRepositoryhas real persistence behavior, so it suits a fake.
3. Stub: Return Preset Answers
A stub is an object that returns fixed values for method calls. A stub doesn’t care how many times it’s called. You also don’t assert anything on the stub. Its sole purpose is to direct the code under test into the correct logical path you want to verify.
3.1. Hand-Written Stub
The simplest way is to implement the interface yourself:
class DecliningPaymentGateway implements PaymentGateway {
@Override
public boolean charge(String customerEmail, BigDecimal amount) {
return false;
}
}
This class always declines the payment. We use it to test the “payment declined” branch:
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();
}
}
In this test:
DecliningPaymentGatewayis a stub. It forcesplaceOrder()to take thereturn falsepath.- The assertion is on the return value of the service, not on the stub.
mock(OrderRepository.class)andmock(EmailSender.class)here merely act as dummies. These are objects passed in to satisfy the constructor parameters but are never actually used.
3.2. Stub With Mockito
Writing a class for each scenario quickly becomes cumbersome. Mockito allows you to create stubs inline in your tests:
@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();
}
Even though it’s created with mock(), paymentGateway here is still a stub in terms of its role: we only configure its return value with when(...).thenReturn(...) and never verify it. The library function name doesn’t determine the test double type. How you use the object is what matters.
4. Mock: Verify Interactions
A mock is an object that records all calls made to it, allowing you to verify afterward that the code called the right method, with the right parameters, the right number of times. Unlike a stub, the assertion is on the mock itself.
Mocks are useful when the behavior you’re verifying is a side effect that doesn’t return anything, like sending an email, publishing an event, or writing an audit log.
@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", "Order ORD-1 has been confirmed");
}
@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);
}
}
Breaking it down:
@ExtendWith(MockitoExtension.class)activates Mockito for JUnit 5. Fields annotated with@Mockare initialized before each test.@InjectMockscreates anOrderServiceand injects the three@Mockobjects via the constructor.paymentGatewayremains a stub, whileemailSenderis now a true mock because weverifyit.verify(emailSender).send(...)verifies thatsend()was called exactly once with the exact two parameters.verifyNoInteractions(...)asserts that when payment fails, the system neither saves the order nor sends an email.
If you accidentally remove the emailSender.send(...) line from OrderService, the first test will fail with a message like this:
Wanted but not invoked:
emailSender.send(
"an@example.com",
"Order ORD-1 has been confirmed"
);
-> at OrderServiceMockTest.givenApprovedPayment_whenPlaceOrder_thenSendConfirmationEmail
Actually, there were zero interactions with this mock.
Note: Don’t stub and verify the same call in one test, like doing both
when(paymentGateway.charge(...))and thenverify(paymentGateway).charge(...). If the stub isn’t called, the service won’t returntrueand the test fails elsewhere anyway. Theverifyhere is redundant and only makes the test tighter to the implementation.
5. Fake: Real Logic, Simplified
A fake is an implementation that actually works but uses shortcuts unsuitable for production. A typical example is a repository that stores data in a HashMap instead of a database:
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));
}
}
Unlike a stub, a fake has logic and state. Calling save() and then findById() returns the exact order you just saved. This allows you to verify the final state instead of checking method calls:
class OrderServiceFakeTest {
private final InMemoryOrderRepository orderRepository = new InMemoryOrderRepository();
private final OrderService orderService = new OrderService(
(email, amount) -> true, // stub as lambda
orderRepository, // fake
(to, subject) -> { }); // dummy as 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);
}
}
A few points worth noting:
PaymentGatewayandEmailSendereach have only one abstract method, so we create the stub and dummy using lambdas without needing Mockito.- The test doesn’t care how many times
OrderServicecallssave()or in what order. It only cares that after placing an order, the order is in the repository. - If you later refactor
OrderServiceto use a different method, likesaveAll(), a test using a fake will still pass as long as the end result is correct. A test usingverify(orderRepository).save(order)would break.
Note: Fakes are code, so they can have bugs too. If a fake behaves differently from the real implementation (like not throwing an error for duplicate keys), your test might pass while production fails. For important fakes, write a set of contract tests that run against both the fake and the real implementation (for example, the JPA version running with Testcontainers).
6. Comparison and Selection
The table below summarizes the key differences:
| Criteria | Stub | Mock | Fake |
|---|---|---|---|
| Purpose | Provides input data | Verifies interactions | Replaces real behavior with a lighter version |
| Has logic? | No, returns fixed values | No, only records calls | Yes, behaves like the real thing |
| Where to assert | On the code’s return value | On the mock itself (verify) |
On the fake’s state |
| Testing style | State-based | Interaction-based | State-based |
| Refactoring resilience | High | Lower | High |
Beyond these three types, you’ll encounter two others:
- Dummy: an object passed in to satisfy parameters but never actually used.
- Spy: wraps a real object, still runs the real logic but records calls for
verify. In Mockito, you create a spy withspy(...).
A simple selection rule:
- If a dependency returns data to your code, use a stub.
- If a dependency is an external side effect (email, message queue, third-party API) with no observable state, use a mock.
- If a dependency has state and is used in many tests (repository, cache, clock), consider writing a fake once and reusing it everywhere.
In general, prefer verifying state (stub, fake) over verifying interactions (mock). State-based tests describe the outcome, while interaction-based tests describe the implementation, and implementations change more often.
7. Common Mistakes
Mocking everything. If a test has five when statements and four verify calls, it’s copying the implementation rather than checking behavior. Simply reordering two lines of code will make the test fail even though the result is still correct.
Mocking value objects. Don’t mock Order, BigDecimal, or LocalDate. Create real instances. Mockito 5 can mock final classes and records by default, but being able to do something doesn’t mean you should.
Mocking types you don’t own. Mocking RestTemplate or a vendor SDK directly makes your test assume the wrong behavior. Instead, wrap the library behind an interface you own (like PaymentGateway) and mock that. Test the real implementation with an integration test.
Unused stubs. With MockitoExtension, Mockito uses strict stubs by default. If you stub a call that never happens, the test fails:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Don’t rush to suppress this with lenient(). The warning usually means your test isn’t taking the path you thought, or your setup is outdated.
Using fake instead of integration tests. InMemoryOrderRepository can’t verify SQL queries, constraints, or transactions. Fakes speed up unit tests, but you still need a few integration tests with a real database.
8. Conclusion
Stub, mock, and fake are all test doubles but answer three different questions. Stub answers “What does the dependency return?” Mock answers “Does the code call the dependency correctly?” Fake answers “If there’s a real but lighter implementation, what’s the final result?”
Choose your test double based on the dependency’s role, not the tool: an object created with mock() can be purely a stub. Prefer state-based verification; only use verify for side effects that truly need checking. Your tests will be shorter, easier to read, and less brittle when refactoring.