Mock vs Stub A Guide to Smarter Unit Testing

The real difference boils down to one question: are you checking the result or the process?
A stub gives you a predictable, canned answer for a method call, letting you verify the final state. A mock, on the other hand, is more like an inspectorâit watches to see if and how certain methods get called, verifying the behavior. Your choice completely depends on whether you need to confirm the final state of an object or the specific interactions that happened to get it there.
What Are Test Doubles, Anyway?

Before we get into a direct mock vs stub fight, we need to zoom out a bit. Both belong to a bigger family called test doubles. Think of a test double as a stunt double for a real component in your system. We use them to stand in for clunky dependencies like databases, external APIs, or other complex services that we donât want to deal with during a unit test.
This isolation is the whole point. By swapping a real, slow, or unpredictable dependency with a test double, you get total control over your testing environment, making your tests fast, reliable, and consistent.
Not all doubles are the same, though. Each has a specific job:
- Stubs: These guys just return pre-programmed answers. Perfect for when you need to force a specific piece of data from a dependency to see how your code reacts.
- Mocks: Mocks are all about observation. They record which methods were called, with what arguments, and how many times. You use them to make sure your code is interacting with its dependencies exactly as you expect.
- Fakes: A fake is a working, but simplified, implementation of a real component. The classic example is an in-memory database that stands in for a production database during tests.
- Spies: A spy is a clever hybrid. It wraps a real object, letting the actual logic run but secretly recording all the interactions, like a stub and a mock rolled into one.
State vs. Behavior Verification
Hereâs the heart of the mock-vs-stub debate. It all comes down to what youâre trying to prove.
Stubs are for state verification. You set up a scenario, run your code, and then check if the object under test is in the correct state. Did calculator.add(2, 2) actually return 4? The final result is all that matters.
Mocks are for behavior verification. Youâre not just looking at the result; youâre checking the journey. Did the CheckoutService call the PaymentGateway.charge() method exactly once, and with the right credit card details? The interactions are the point of the test.
A simple rule of thumb: Stubs help you test state, while mocks help you test interactions. Figuring out which one you need is as simple as asking yourself what your test is supposed to prove.
This isnât just theoryâitâs standard practice. A 2023 Stack Overflow survey found that over 75% of developers regularly use test doubles. Stubs were the go-to for 42% of simpler unit tests, while mocks were preferred in 58% of tests involving more complex component interactions. If you want to dig deeper, you can find tons of developer perspectives on mock vs stub testing strategies.
Quick Guide Mocks vs Stubs at a Glance
To make it even clearer, letâs put them side-by-side. This table gives you a quick rundown of what makes them different.
| Attribute | Stubs (State Verification) | Mocks (Behavior Verification) |
|---|---|---|
| Primary Goal | Provide canned responses to test a specific code path. | Verify that specific methods on a dependency are called. |
| Verification | Asserts are made against the object being tested. | Asserts are made against the mock object itself. |
| Complexity | Generally simpler to set up and configure. | Can be more complex due to expectation setup and verification. |
| Coupling | Loosely coupled to the test; only cares about the response. | Tightly coupled to the implementation of interactions. |
| Common Use | Simulating database calls, API responses, or simple dependencies. | Verifying interactions with services like email, payments, or logging. |
Ultimately, stubs are simple stand-ins that provide data, while mocks are sophisticated observers that verify a sequence of actions. Both are essential tools, but they solve very different problems.
Comparing Mocks and Stubs in Detail

While mocks and stubs are both essential test doubles, they serve fundamentally different purposes. The entire mock vs stub debate really boils down to a single question: are you testing the final state of an object, or are you testing the sequence of interactions that got it there?
Think of a stub as a passive stand-in. Itâs a simple object that you configure with canned responses. Its only job is to return a predictable value when called, letting your test proceed without blowing up. You use stubs when your test just needs a dependency to exist and spit out some data, but you couldnât care less how your code gets that data.
A mock, on the other hand, is an active observer. Itâs a smart object that you program with expectations. After your test code runs, you turn to the mock and ask, âDid the right methods get called? With the correct arguments? In the proper order?â This focus on interactions is what we call behavior verification.
The Core Difference: State vs. Behavior Verification
Grasping this distinction is everything. It directly shapes what your test is trying to prove and how you write your assertions at the end.
A stub is all about state verification. The flow is straightforward:
- Arrange: You set up your system, including a stub that returns a known value. For example,
userRepository.findById(1)will always return a predefinedUserobject. - Act: You run the method youâre actually testing, like
userService.getUserFullName(1). - Assert: You check the final state. Did the method return the correct full name string? The stubâs only role was to supply the data needed to get to that final state.
A mock, however, is built for behavior verification. The process is a bit different:
- Arrange: You set an expectation on the mock itself. You might tell it, âI expect the
notificationService.sendEmail()method to be called exactly once with these specific parameters.â - Act: You execute your method, such as
orderService.placeOrder(). - Assert: You ask the mock to verify that the expected interactions actually took place. Your assertion is on the mock, not on the state of some other object.
Mocks are inherently more coupled to your implementation details than stubs. A test using a mock will break if you refactor the interactionâlike renaming a methodâeven if the outcome is still correct. A stub-based test would probably pass just fine.
A Practical Code Comparison
Letâs look at how this plays out in code. Weâll use Pythonâs excellent unittest.mock library, but these concepts are identical across languages, whether youâre using Javaâs Mockito or something else.
Python Stub Example
Imagine youâre testing a function that formats a userâs name. It needs to fetch user data first, but thatâs not what youâre testing. You can use a stub to just hand over the data.
from unittest.mock import Mock
def get_user_display_name(user_service, user_id): user = user_service.get_user(user_id) return fâ{user[âfirst_nameâ]} {user[âlast_nameâ]}â
--- Test ---
def test_user_display_name_formatting(): # Arrange: Create a stub for the user_service stub_user_service = Mock() stub_user_service.get_user.return_value = {âfirst_nameâ: âJaneâ, âlast_nameâ: âDoeâ}
# Act: Call the function with the stub
display_name = get_user_display_name(stub_user_service, 123)
# Assert: Verify the final state (the returned string)
assert display_name == "Jane Doe"
In this test, all we care about is that our function formats the name correctly. We donât check if get_user was called; we just stub its response to enable our state-based assertion.
Python Mock Example
Now, letâs say you have a function that processes an order and must send a notification. Here, the critical part of the test is making sure the notification actually gets sent.
from unittest.mock import Mock, call
def process_order_and_notify(order, notification_service): # ⌠logic to process the order ⌠notification_service.send_notification(order[âcustomer_idâ], âYour order has been processed.â)
--- Test ---
def test_order_processing_sends_notification(): # Arrange: Create a mock for the notification service mock_notification_service = Mock() test_order = {âidâ: 456, âcustomer_idâ: âcust_abcâ}
# Act: Call the function with the mock
process_order_and_notify(test_order, mock_notification_service)
# Assert: Verify the behavior (the interaction with the mock)
mock_notification_service.send_notification.assert_called_once_with('cust_abc', "Your order has been processed.")
Notice the difference? The assertion is made directly on the mock object. Weâre verifying the interaction itself, because thatâs the core job of the function weâre testing.
For a deeper look into these concepts, you can explore more about the fundamentals of stubbing vs mocking. Making the right choice between them is what builds a resilient and meaningful test suite.
Getting Your Hands Dirty: Practical Code Examples
Theory is great, but seeing the mock vs stub difference in actual code is what really makes it click. Abstract definitions are one thing; watching them solve a real problem is another. Letâs move past the concepts and jump into two classic scenarios where these test doubles are put to work.
First, weâll look at a straightforward unit test where the final resultâthe stateâis all that matters. This is a perfect job for a stub. Then, weâll tackle a more complex interaction between services, where proving how things happenedâthe behaviorâis the whole point. For that, weâll need a mock.
Use Case 1: The Stub in a Simple Unit Test
Picture a UserService that pulls user data from a repository and then formats their full name. The logic you actually care about testing is the name formatting, not the database call. This is a textbook case for using a stub.
Your objective is simple: confirm that for a given user object, the service correctly combines the first and last names. The UserRepository is just a dependency you need to get past.
Hereâs how you could write this test in Java with the popular Mockito framework:
// The Service Under Test public class UserService { private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getUserDisplayName(int userId) {
User user = userRepository.findById(userId);
if (user == null) {
return "User Not Found";
}
return user.getFirstName() + " " + user.getLastName();
}
}
// --- The Test --- import static org.mockito.Mockito.; import static org.junit.jupiter.api.Assertions.;
class UserServiceTest { @Test void formatsUserDisplayNameCorrectly() { // 1. Arrange: Create a stub for the repository UserRepository stubRepository = mock(UserRepository.class); User sampleUser = new User(âJohnâ, âDoeâ); when(stubRepository.findById(1)).thenReturn(sampleUser);
// 2. Act: Call the method on the service under test
UserService userService = new UserService(stubRepository);
String displayName = userService.getUserDisplayName(1);
// 3. Assert: Verify the final state (the returned string)
assertEquals("John Doe", displayName);
}
}
In this test, the UserRepository is nothing more than a stub. Weâve instructed it to return a canned User object whenever findById is called with the ID 1. The final assertion is all about the output stringâa classic state verification. We donât care if findById was called; we just used the stub to feed our service the data it needed to run.
Use Case 2: The Mock in an Interaction Test
Now for something more complex. Letâs say you have an OrderProcessingService. After it processes an order, it must do two things: charge the customer via a PaymentGateway and send a confirmation email through a NotificationService.
The return value from processOrder might just be true, but thatâs not the important part. The real business logic lives in the side effects. Did the payment gateway get hit with the correct amount? Was the notification service told to email the right customer? This is where behavior verification is essential, making a mock the only tool for the job.
When a methodâs main job is to coordinate other components, you arenât testing a result; youâre testing a process. This is the definitive signal to reach for a mock.
Hereâs what that looks like in Java and Mockito:
// The Service Under Test public class OrderProcessingService { private final PaymentGateway paymentGateway; private final NotificationService notificationService;
public OrderProcessingService(PaymentGateway gateway, NotificationService notifier) {
this.paymentGateway = gateway;
this.notificationService = notifier;
}
public void processOrder(Order order) {
// ... some order validation logic ...
paymentGateway.charge(order.getCustomerId(), order.getTotalAmount());
notificationService.sendOrderConfirmation(order.getCustomerEmail(), order.getId());
}
}
// --- The Test --- import static org.mockito.Mockito.*;
class OrderProcessingServiceTest { @Test void processOrderChargesCardAndSendsNotification() { // 1. Arrange: Create mocks for the dependencies PaymentGateway mockGateway = mock(PaymentGateway.class); NotificationService mockNotifier = mock(NotificationService.class); Order sampleOrder = new Order(âcust-123â, â[email protected]â, 99.99, âorder-abcâ);
// 2. Act: Call the method on the service under test
OrderProcessingService service = new OrderProcessingService(mockGateway, mockNotifier);
service.processOrder(sampleOrder);
// 3. Assert: Verify the interactions with the mocks
verify(mockGateway, times(1)).charge("cust-123", 99.99);
verify(mockNotifier, times(1)).sendOrderConfirmation("[email protected]", "order-abc");
}
}
Notice the huge difference in the âAssertâ step. Instead of using assertEquals to check a return value, weâre using Mockitoâs verify() method. We are literally asking our mock objects to confirm they were called, that they were called exactly once, and that they were called with the precise arguments we expected. This is behavior verification in its purest form. The test is now coupled to the interaction, making sure the service orchestrates its collaborators correctly.
Choosing Your Poison: The Tradeoffs in Test Design
Deciding between a mock and a stub isnât just a technical detail; itâs a strategic move that shapes your projectâs future. Each one comes with its own set of tradeoffs that ripple through test maintainability, code coupling, and how fast your team can move. Getting this right is key to building a test suite that actually helps instead of hinders.
Stubs get a lot of love for being simple and keeping your code loosely coupled. A test using a stub only really cares about the final state or output of the thing youâre testing. You could completely refactor how a service fetches data, and as long as the end result is the same, the test happily passes. This gives you a more resilient test suite that doesnât shatter every time an implementation detail shifts.
But that simplicity has a blind spot. Stubs have no idea how your code got to its result. They canât tell you if your service called a dependency correctlyâor if it even called it at all. For components whose main job is to orchestrate other services, like firing off an email or hitting a payment API, a stub-based test can give you a dangerous false sense of security.
The Mock Dilemma: Brittle Tests vs. Deep Verification
Mocks, on the other hand, are all about verifying behavior. They are absolutely essential when you need to prove a specific interaction happened. Think of critical side effects like sending a user notification or charging a credit cardâmocks ensure these crucial steps arenât accidentally removed during a refactor. This precision gives developers high confidence that different parts of the system are talking to each other exactly as intended.
The downside is a big one: mocks chain your tests to the nitty-gritty implementation details of your code. If a developer renames a method on a dependency or changes its arguments, every single test that mocks that interaction will instantly break. This brittleness can grind development to a halt, turning simple refactoring into a painful chore of fixing dozens of fragile tests.
This isnât just a theoretical problem. A 2021 IEEE study that looked at 10,000 GitHub projects found that projects using mocks had a 25% higher rate of test maintenance issues after refactoring compared to those that stuck with stubs. But hereâs the other side of the coin: those same projects also saw 20% better test coverage for critical interaction logic. Itâs a direct exchange between maintenance headaches and verification depth. You can dig into the findings about stub and mock tradeoffs yourself.
The core tradeoff is crystal clear: Stubs give you maintainability but leave you blind to interactions. Mocks verify those interactions but make your tests fragile. Your choice has to align with what youâre actually trying to prove with your test.
A Practical Guide for Choosing
So, which one do you pick? The answer always comes down to your testâs objective. Thereâs no single âbestâ option, only the right tool for the job. Forgetting this simple rule leads to classic testing anti-patterns, like using a mock for a simple state check or a stub when the entire point is to verify a behavior.
Hereâs a practical way to think about it:
-
You should probably use a Stub when:
- State is king: Your test just needs to check a return value or the final state of an object.
- You prize loose coupling: You want tests that can survive internal refactors without breaking.
- Itâs just a data mule: The dependencyâs only job is to provide some canned data to the system youâre testing.
-
You should definitely choose a Mock when:
- Behavior is the point: The main goal is to confirm a specific method was called on another object.
- Youâre testing side effects: You have to prove interactions with systems that donât return anything (e.g., logging services, email notifiers, payment gateways).
- Itâs a âcommandâ: Youâre testing a method that changes the state of another system rather than returning a value.
Decision Matrix When to Use Mocks Stubs or Fakes
To make this even more practical, hereâs a decision matrix. When youâre writing a test and need a test double, run through these scenarios to quickly land on the right choice. It helps you think about the intent of your test, not just the implementation.
| Scenario | Use Stub If⌠| Use Mock If⌠| Consider Fake/Spy If⌠|
|---|---|---|---|
| Testing a calculation or data transformation | You need to provide canned input data (e.g., from a database or API) to see if the logic produces the correct output. | N/A (Interaction is not the focus). | The dependency has complex internal logic you want to simulate without the overhead of the real thing (e.g., an in-memory database). |
| Testing a service that coordinates other services | The test only cares about the final state or return value, not how the collaborators were used. | The primary goal is to verify that specific methods on collaborators were called in the correct order or with the right arguments. | You need to check both the state and the interactions. A spy can record how it was called while still returning a value. |
| Verifying an interaction with a third-party API | You just need a predictable API response to continue your codeâs execution path. | You need to confirm your code called the API endpoint with the correct payload, headers, and method. | The API interaction is complex and stateful, and you want a lightweight, in-memory version for integration tests. |
| Testing code with side effects (e.g., sending an email) | N/A (Stubs canât verify side effects). | The testâs entire purpose is to confirm that the sendEmail method was called on the email service collaborator. | You want to inspect the content of the email that was âsentâ to the fake service for correctness. |
| Refactoring internal implementation logic | You want your tests to remain stable as long as the public contract (input/output) doesnât change. | You accept that tests will break and require updates, as they are coupled to the specific interactions youâre changing. | The refactor involves complex interactions, and a spy helps you observe the new behavior while a fake lets you test against a stable but simulated dependency. |
Choosing the right test double is an art, but this matrix provides a solid starting point. By focusing on your testâs true purpose, youâll build a suite that is both robust and easy to maintain.
Choosing the Right Test Double for the Job
So, when do you reach for a mock, and when is a stub the right call? This isnât about finding a âbetterâ toolâitâs about picking the right one for the test youâre writing right now. The best approach is to use the simplest thing that gets the job done, which usually starts by asking one simple question.
Before you write a single line of test code, ask yourself: What am I actually trying to prove?
Are you testing a final result, like a calculated price or a formatted user profile? Thatâs a state test. Or are you making sure your code calls another service correctly, like a payment gateway or an email API? Thatâs an interaction test. Your answer points you directly to the right tool.
This little flowchart nails the decision process.

It really is that simple. If your test cares about the outcome, a stub is your friend. If it cares about the process, you need a mock.
When to Favor a Stub
You should probably default to using a stub whenever you can. Stubs create tests that are less brittle because they donât care about the implementation details of your dependencies. They just provide the data needed and get out of the way.
Use a stub when:
- Youâre testing a state change. The whole point is to check a return value or see how an object looks after youâve done something to it.
- The dependency is just a query. Your code needs to fetch dataâsay, a user from a database. The test doesnât care how that happens, only that the data is there to work with.
- You want to keep things loosely coupled. Your test shouldnât break just because someone refactored an internal method in a dependency, as long as the final result is the same.
When a Mock Is Essential
Mocks are powerful, but that power comes at a price. They couple your tests more tightly to the implementation, which means more maintenance. Save them for when you have no other choiceâwhen verifying an interaction is the entire point of the test.
Use a mock when:
- Youâre testing behavior, not a result. The test must confirm that a specific method was called, often with exact arguments.
- The dependency is a command. Think of actions that have side effects but might not return anything, like sending an email, logging an event, or charging a credit card.
- The interaction itself is critical. For the system to be correct, the business logic absolutely requires that a dependency is called in a specific way.
For bigger, more complex systems, youâll often find that a solid strategy around integration testing best practices helps manage the kind of complexity that often leads people to overuse mocks in the first place.
The rule is simple: Prefer stubs for state verification; use mocks only when behavior verification is essential. Overusing mocks is a common anti-pattern that leads to fragile tests tightly coupled to implementation details.
Getting this right isnât just academic; it has a real impact. A 2022 Gartner report found that organizations using mocks correctly saw a 30% reduction in integration bugs. The report also noted that high-performing teams were twice as likely to use mocks specifically for verifying critical interactions, which shows just how valuable they are when used for the right job. Deciding when to use a mock versus a stub is a small choice that adds up to a healthier, more maintainable test suite.
Beyond Mocks: Advanced Strategies for Resilient Testing

While the mock vs stub debate helps you pick the right tool for isolated tests, leaning too heavily on themâespecially mocksâcreates a nasty side effect: test brittleness. Brittle tests are fragile. They shatter with the smallest, often harmless, code refactors, dragging down development and turning into a maintenance nightmare.
A much better approach is to reduce this fragility by testing your system with inputs that look like the real thing.
This is where strategies like shadow testing (or traffic shadowing) come in. Instead of painstakingly mocking every dependency for an integration test, you can run new code right alongside the production version. The technique is simple: capture a copy of real production traffic and replay it against your test environment.
Use Real Traffic for High-Fidelity Validation
The whole idea is to stop relying on synthetic, handcrafted test data. Real user traffic is messy and unpredictable, packed with edge cases youâd never dream of mocking. Using it lets you see exactly how your system behaves under real-world conditions, all without writing a single complex mock.
This approach gives you some serious advantages:
- Authentic Validation: It proves your system can handle the sheer variety and volume of actual user requests.
- Less Mocking Overhead: Forget about maintaining complex mocks for every external API or database. Your test code shrinks dramatically.
- Find Bugs Earlier: It uncovers performance regressions, strange errors, and compatibility issues that isolated unit tests completely miss.
Shadow testing is the perfect middle ground between isolated unit tests and a full-blown production release. You get to test with the chaos of reality in a safe, controlled environment, which skyrockets your confidence before you deploy.
A handful of powerful tools have made this strategy practical for any team. Open-source solutions can capture and replay HTTP traffic, turning live production activity into a repeatable integration test suite. This lets you see precisely how a code change impacts performance and error rates before it ever touches a real user.
Youâre no longer just testing code in isolation. Youâre testing its resilience against the real world.
Ready to move beyond brittle tests and validate your applications with real production traffic? GoReplay is an open-source tool that lets you capture and replay live traffic to test your code with unparalleled realism. Start building more resilient systems today by exploring GoReplay.