Stubs vs Mocks A Complete Developer Guide

November 28, 2025
21 min read

The core difference between a stub and a mock really boils down to one question: are you testing state or are you testing behavior?

Stubs are all about providing pre-programmed answers to method calls so your test can run. Mocks, on the other hand, are there to verify that specific methods were actually called. Your choice hinges on whether you need to check the final result of an operation or confirm the sequence of interactions that led to it.

Understanding The Difference Between Stubs And Mocks

When you're building software, you can't have reliable applications without solid testing. A fundamental principle of unit testing is to check a piece of code in complete isolation, away from its dependencies like databases, external APIs, or other microservices. This is where test doubles come into play, and stubs and mocks are two of the most common tools in the box.

On the surface, they both look similar—they're stand-in objects that mimic real components. But they have very different jobs.

  • Stubs are for state verification. They are programmed with canned, hard-coded responses. Think of a stub as a simple actor in a play; it knows its lines and delivers them on cue, allowing the scene (your test) to continue without a hitch.
  • Mocks are for behavior verification. These are more like inspectors. They watch the interactions between your code and its dependencies, carefully recording which methods were called, how many times, and with what specific arguments. The test then interrogates the mock to confirm if everything happened as expected.

Essentially, stubs provide the state your test needs to run, while mocks verify the interactions along the way. To get a better handle on where these tools fit in the larger picture, it's helpful to review the main types of software testing. Understanding that context really clarifies why isolating components with test doubles is so critical.

A desk with an iMac displaying 'stub' and a laptop, illustrating 'STUBS vs Mocks' concepts.

Core Purpose Of Test Doubles

By using stubs and mocks, you create tests that are fast, predictable, and completely isolated. When you replace a slow or flaky dependency (like a network call to a third-party API) with a test double, you make sure your test is focused only on the logic of the component you’re actually trying to examine.

This isolation is the whole point. If a test fails, you know immediately that the problem is within the unit under test, not lurking somewhere in a complex web of dependencies. It makes debugging exponentially faster and your entire test suite far more reliable.

A good unit test has to be independent and repeatable. Test doubles like stubs and mocks are the tools that get you there by breaking dependencies and giving you full control over the test environment.

Stubs vs Mocks At A Glance

To put it all together, here’s a quick summary of the primary differences between stubs and mocks, zeroing in on their verification style, purpose, and where you'd typically use them.

Attribute Stubs Mocks
Primary Goal Provides canned data to let the test run. Verifies that specific methods were called.
Verification State Verification (checks the final result). Behavior Verification (checks the interactions).
Complexity Simple, often just returns a fixed value. More complex, involves setup and verification steps.
Test Focus On the outcome of the method under test. On the communication between objects.
Common Use Case Simulating a database call that returns a user object. Ensuring a notification service's send() method is called.
Analogy A vending machine that always gives the same snack. A supervisor watching to ensure a procedure is followed.

This table should help you quickly decide which tool is right for the job at hand. It's all about choosing the test double that best fits what you're trying to prove with your test.

Comparing State vs. Behavior Verification In Depth

Choosing between stubs and mocks isn't just a technical detail; it’s a decision that fundamentally shapes how you think about and write your tests. It all comes down to a single question: are you testing what your code produces (its state) or how it gets there (its behavior)? This is the real heart of the stubs vs. mocks debate, and the answer has big consequences for your test suite's design, complexity, and long-term health.

Stubs are all about state verification. When you use a stub, your test is focused on the end result. You set up some initial conditions, run your code, and then assert that the final state is what you expected. The internal journey doesn't matter, only the destination.

Mocks, on the other hand, are built for behavior verification. Here, the journey is everything. Your test is designed to prove that your code correctly collaborated with its dependencies. You assert that certain methods were called, maybe in a specific order or with the exact right parameters.

Smartphone showing California map with red star and 'State vs Behavior' text, beside a running figurine.

Impact On Test Design And Coupling

This choice has a direct impact on how tightly your tests are coupled to your implementation details. A test built around state verification tends to be more resilient to refactoring. If you find a better way to implement a method—but it still produces the same correct output—a stub-based test will keep passing. That’s a good thing, because it lets you improve your code without breaking a dozen tests.

Behavior verification with mocks creates a much tighter bond. If you refactor a method to call a different helper function on a dependency, your mock-based test will fail, even if the final result is identical. Why? Because the test was explicitly verifying an interaction that no longer exists.

The core trade-off is this: Mocks can catch interaction bugs that stubs might miss, but they can also create brittle tests that break during harmless refactoring. State-based tests are more robust but may overlook incorrect collaboration patterns between objects.

This brittleness is one of the most common complaints you'll hear about mock-heavy testing. When tests fail because of internal implementation changes instead of actual bugs, developers start losing trust in the test suite. It slows things down, turning a simple refactor into a tedious job of updating both production code and the tests that are now unnecessarily broken.

Analyzing Setup Complexity

You'll also notice a real difference in the setup complexity, which reflects what each test double is trying to accomplish.

  • Stub Setup: This is usually dead simple. You just configure the stub to return a canned response when a method is called. It’s a quick, one-step process focused on getting the necessary data into your test.
  • Mock Setup: This is a bit more involved. You have to set expectations on the mock object before running your code, then execute the code, and finally, explicitly verify that the expected interactions actually happened. This three-part "setup, execute, verify" pattern is what adds the complexity.

For example, stubbing a UserRepository to return a known user object is straightforward. But mocking an EmailService means you have to tell the mock to expect a call to its send() method with a specific subject and recipient, and then check that it happened afterward. The extra setup is the price you pay for confirming behavior.

The Role Of Contract Testing

The risk of mocks becoming too tangled with implementation details underscores the need for stable interfaces. Behavior verification is much safer when the "rules" of interaction are well-defined and unlikely to change. This is where related disciplines like contract testing come in. To see how this works in practice, check out our guide on what is contract testing, which dives into ensuring services communicate correctly without creating fragile integration tests.

Ultimately, the decision isn't about whether stubs or mocks are universally "better." A healthy, well-balanced test suite uses both. Stubs are fantastic for providing data from stable dependencies, while mocks are powerful for verifying critical side effects—like sending a notification or processing a payment—where the interaction itself is the feature. Choosing wisely is the key to building a test suite that's both effective and maintainable.

Getting Your Hands Dirty: Code Examples for Stubs and Mocks

Theory is great, but code is where the rubber meets the road. Let’s move past the abstract definitions and look at how stubs and mocks actually work in practice. We'll use Python and its built-in unittest.mock library, a go-to for creating test doubles, to explore two common, everyday scenarios.

The point here isn’t just to show you how to write the code, but to build an intuition for why you’d pick a stub in one situation and a mock in another.

Example 1: Using a Stub for State Verification

Let's say you have a UserService that figures out if a user is an administrator. This service relies on a UserRepository to fetch user data from a database. Hitting a real database in a unit test is a bad idea—it’s slow, brittle, and introduces external dependencies. This is the perfect job for a stub.

Our test's mission is to check the logic inside the is_admin() method. We don't really care how the user data is retrieved; we just need to supply it to see if our method gives the right answer.

First, here’s the production code we're testing:

Production Code (user_service.py)

class User:
def init(self, name, is_admin):
self.name = name
self.is_admin = is_admin

class UserRepository:
def get_user(self, user_id):
# In the real world, this would hit a database
raise NotImplementedError("This should not be called in a unit test")

class UserService:
def init(self, user_repository):
self.user_repository = user_repository

def is_admin(self, user_id):
    user = self.user_repository.get_user(user_id)
    return user is not None and user.is_admin

Now, let's write a unit test. We'll use a stub to stand in for the UserRepository and feed our UserService a predefined User object. The test cares about one thing: the final boolean value returned by is_admin().

Test Code (test_user_service.py)

import unittest
from unittest.mock import MagicMock

Assume UserService, User, and UserRepository are imported

class TestUserService(unittest.TestCase):
def test_is_admin_returns_true_for_admin_user(self):
# 1. Set up the stub
stub_user_repository = MagicMock(spec=UserRepository)
admin_user = User(name="Alice", is_admin=True)
stub_user_repository.get_user.return_value = admin_user

    # 2. Run the code
    service = UserService(stub_user_repository)
    result = service.is_admin(123)

    # 3. Assert the state (the result)
    self.assertTrue(result)

See how that works? The stub_user_repository is hardcoded to return our admin_user whenever get_user is called. We don't check if it was called—we just provide a canned response so we can verify the outcome of our is_admin logic. The test is simple, fast, and laser-focused on the state of the returned value.

Example 2: Using a Mock for Behavior Verification

Let's switch gears. Imagine an OrderService that processes an order and then sends a confirmation email using a NotificationService. We definitely don't want to send a real email during a test. That's slow, unreliable, and has messy side effects.

In this case, the return value of process_order might just be True, which doesn't tell us much. The most important part of this operation is the interaction—making sure the notification service was actually called. This is a classic mock scenario.

Here's the production code:

Production Code (order_service.py)

class NotificationService:
def send_confirmation(self, email_address, order_id):
# This would send a real email
raise NotImplementedError("This should not be called in a unit test")

class OrderService:
def init(self, notification_service):
self.notification_service = notification_service

def process_order(self, order_id, customer_email):
    # ... some complex order processing logic ...
    print(f"Processing order {order_id}...")

    # After success, send the notification
    self.notification_service.send_confirmation(customer_email, order_id)
    return True

The test must confirm that send_confirmation was called exactly once, and with the right arguments. A mock is perfect for this because it acts like a spy, recording the interactions so we can verify them later.

Test Code (test_order_service.py)

import unittest
from unittest.mock import MagicMock

Assume OrderService and NotificationService are imported

class TestOrderService(unittest.TestCase):
def test_process_order_sends_confirmation_email(self):
# 1. Set up the mock
mock_notification_service = MagicMock(spec=NotificationService)

    # 2. Run the code
    service = OrderService(mock_notification_service)
    service.process_order(order_id=987, customer_email="[email protected]")

    # 3. Verify the behavior (the interaction)
    mock_notification_service.send_confirmation.assert_called_once_with(
        "[email protected]", 987
    )

Key Takeaway: The stub example asserted the final state (self.assertTrue(result)), while the mock example verified the behavior (assert_called_once_with(...)). This distinction is the core difference and the most important factor when choosing between them.

By using a mock, we prove that OrderService plays nicely with its collaborators. We don't care what send_confirmation returns; we only care that it was called correctly as a side effect of our business logic. This gives us confidence that the different parts of our system are wired together properly.

When To Use A Stub And When To Use A Mock

Picking the right tool for the job is everything in testing. When it comes to test doubles, choosing between a stub and a mock isn't just a technical detail—it directly shapes how effective and maintainable your tests will be down the road. The decision really boils down to a single question: what, specifically, are you trying to prove with this test?

Getting this right from the start helps you build a test suite that's both accurate and resilient, one that doesn't break every time you refactor a minor detail. The fundamental rule is surprisingly simple. If your test cares about the result of an action, you need a stub. If it cares about the action itself, you need a mock.

Choosing Stubs For State Verification

You should reach for a stub when your test's main goal is to check the final outcome or state of the code you're testing. Think of it this way: the dependency is just there to provide a piece of information, and you don't really care how your code gets it. You just care what your code does with it.

Stubs are perfect for situations like these:

  • Retrieving Data: Your code needs to fetch a user profile from a database to check their permissions. A stub can serve up a fake user object every single time, without ever hitting a real database.
  • Checking Configuration: You need to test how your system behaves when a feature flag is enabled. A stub can be programmed to return true or false on command, letting you test both paths easily.
  • Simple Queries: Any time a dependency is just a lookup service. The goal is simply to get some data so the test can proceed, not to verify that the lookup actually happened.

Use a stub when your test is asking, "Given this specific input from a dependency, does my method produce the correct output?" The interaction is just a means to an end.

This flowchart breaks down that decision-making process into a simple visual guide.

Flowchart illustrating when to use Stub for data and Mock for behavior in software testing.

As you can see, the path is clear. If your test needs predefined data to check an outcome, a stub is your tool. If it needs to validate that an action took place, you're heading toward a mock.

Opting For Mocks For Behavior Verification

A mock is what you need when the interaction with a dependency is the feature you're testing. In these scenarios, the side effect—the method call itself—is the whole point. Mocks are like vigilant observers, making sure your code does what it's supposed to do when interacting with others.

Here are some classic use cases for mocks:

  • Sending Notifications: After a customer places an order, you need to be certain an email confirmation was sent. A mock can verify that the sendEmail() method was called with the correct customer address and order details.
  • Logging Events: You want to ensure critical errors are always logged. A mock logging service can confirm that its logError() method was invoked when an exception occurred.
  • Triggering External Systems: Your code processes a payment by calling a third-party API. A mock can stand in for that API and confirm that the call to process the payment was made correctly.

This approach fits perfectly with modern development practices. If you're interested in how this mindset influences software design, our guide on what is test-driven development is a great place to learn more about how writing tests first can lead to cleaner code.

Ultimately, a healthy test suite uses a smart balance of stubs and mocks. Overusing mocks for simple state checks creates brittle tests that are a nightmare to refactor. But using a stub when you really need to verify a critical interaction can let dangerous bugs slip through. By always asking yourself whether you're testing state or behavior, you'll make the right choice every time.

Managing Test Maintenance And Brittleness

Your choice between a stub and a mock isn't just about getting a single test to pass. It has real, long-term consequences for the health and sanity of your entire test suite. Both are essential for isolating code, but how they affect test brittleness—that annoying tendency for tests to break after minor, unrelated code changes—is a huge deal. Getting this right is a balancing act between solving today's testing problem and avoiding tomorrow's maintenance headache.

Mocks, because they're all about verifying how something is done, can accidentally tie your tests directly to the nitty-gritty implementation details of your code. A test that asserts a specific sequence of method calls will immediately fail the second a developer refactors that logic, even if the end result is exactly the same. This creates a stream of frustrating false negatives; the tests break not because of a bug, but because an internal detail was tweaked.

A laptop, notebook, and pencil sit on a wooden desk, with a blue banner reading 'AVOID BRITTLE TESTS'.

This tight coupling isn't just an annoyance; it has a real cost. Some industry reports show that overusing detailed mocks can inflate the cost of refactoring by as much as 25-30%. All that extra time is spent just updating failing tests and their mock setups. That's a serious drag on your team's velocity.

Balancing Brittleness With Bug Detection

On the flip side, stubs generally give you more resilient tests, but they come with their own flavor of risk. Since stubs are just there to provide canned answers and don't verify interactions, they can sometimes create false positives. Your test might pass because it got the data it needed, but it's completely blind to a critical side effect that never happened—like an important notification never being sent or an error never being logged.

This leaves you walking a fine line:

  • Mock-heavy tests are very sensitive. They're great at catching interaction-based bugs early, but they also crank up maintenance costs and make refactoring a pain.
  • Stub-heavy tests are tough. They don't break easily when you change implementation details, but they can miss bugs where two components stop talking to each other correctly.

The goal isn't to pick a favorite and abandon the other. It's about using them strategically. Mock only the critical interactions that define the behavior you're testing, and stub everything else.

Practical Strategies For Maintainable Tests

To keep your test suite from becoming a liability, you have to adopt smarter practices. When you're thinking about your approach, considering agile development best practices can help frame how to use these tools efficiently. Always focus on testing an object's public contract—what it promises to do—not how it does it internally.

Strong test management is also non-negotiable if you want to prevent a brittle test suite from spiraling out of control. For a deeper dive into keeping your tests organized for the long haul, check out our guide on https://dotmock.com/blog/managing-test-cases. By thoughtfully using stubs for state and mocks for truly critical behaviors, you can build a test suite that's both a reliable safety net and a pleasure to work with.

Common Questions About Stubs and Mocks

Even after you get the hang of the basics, the world of test doubles can throw you a curveball now and then. The fine line between stubs and mocks often sparks the same questions among developers, no matter how experienced they are. This section tackles those common points of confusion head-on.

Think of this as your field guide for those tricky, practical challenges that pop up mid-sprint. We'll clear up the gray areas so you can use these tools correctly and confidently in your day-to-day work.

Can You Use Stubs and Mocks Together in the Same Test?

Absolutely. In fact, for many real-world tests, it's the most practical approach. A single unit of work often leans on multiple dependencies, and each one might need a different kind of test double to properly isolate the code you're actually testing.

Let's say you're testing a CheckoutService. Its job is to grab a product's price from a ProductRepository and then charge the customer using a PaymentGateway. A solid unit test for this service would need to do two distinct things:

  1. Use a stub for the ProductRepository to feed the test a fixed, predictable price. Your test just needs this piece of data to proceed; it couldn't care less how the repository was called. That's classic state verification.
  2. Use a mock for the PaymentGateway. Here, you need to verify that its charge() method was called exactly once with the correct amount. The interaction itself is the entire point of the test.

By combining them, you get a test that's both robust and focused. You stub the simple data provider and mock the critical action-taker. This ensures every part of your service's logic is validated without getting bogged down in unnecessary details. The rule of thumb is simple: let the goal of your test dictate the tool you use for each dependency.

What Are the Most Popular Mocking Frameworks?

While you could write your own test doubles from scratch, it's almost always a better idea to use an established framework. These libraries give you a powerful, clean API for creating stubs, mocks, and spies, which saves a ton of boilerplate and effort. The one you choose will naturally depend on your programming language.

Here are a few of the heavy hitters you'll see in the wild:

  • Java: Mockito is the de facto standard. Its clean, fluent API makes it incredibly intuitive to create mocks, stub return values, and verify interactions.
  • Python: The standard library comes with unittest.mock, a powerful and versatile module that's the go-to for most Python developers.
  • .NET (C#): Moq and NSubstitute are the two most popular choices. Both are celebrated for their readable syntax and rich feature sets.
  • JavaScript/TypeScript: The ecosystem here is buzzing with options. Jest has fantastic mocking capabilities built right in. Sinon.js is another excellent standalone library that plays well with just about any testing framework.

These tools handle all the low-level work of managing test doubles, letting you focus on what really matters: writing clear, expressive tests.

How Do Stubs and Mocks Relate to Fakes and Spies?

Stubs and mocks are just two members of a larger family of objects known as test doubles. They're the most common, for sure, but knowing the other members helps you speak the language of test isolation with precision.

  • Fake: A Fake is an object with a simplified, but working, implementation. It's not just programmed with canned answers like a stub; it has actual logic, but it's too lightweight or simplistic for production. The classic example is an in-memory database that stands in for a real SQL server during tests.
  • Spy: A Spy is like a mock that also keeps a record of how it was used. The key difference is that a spy often wraps a real object, letting the actual method run while it "spies" on the interaction. This is perfect when you need to verify a side effect without completely replacing the dependency's behavior.

Key Insight: Think of test doubles on a spectrum of complexity. Stubs are simple data providers. Mocks verify interactions. Spies record interactions while letting the real logic run. Fakes provide a complete, but simplified, alternative implementation.

Is It Bad Practice for Mocks to Return Other Mocks?

Yes, this is almost always a sign that something has gone wrong. If you find yourself writing test setups like mockService.getRepository().findUser(), it's a huge red flag that your code is probably violating the Law of Demeter (also known as the Principle of Least Knowledge).

The principle says an object should only talk to its immediate "friends," not to the friends of its friends. A chain of method calls like that one means your code is reaching deep into the guts of its dependencies. This leads to incredibly brittle tests. If the internal structure of the Service changes—even a little—your test will shatter, even if the high-level logic is still perfectly valid.

Instead of daisy-chaining mocks, take a step back and consider a refactor. A better approach is often to introduce a new method on the Service that directly gives you what you need, hiding that internal path from the outside world. This improves encapsulation and results in tests that are far easier to maintain.


Ready to eliminate testing bottlenecks caused by unavailable or unreliable APIs? dotMock lets you create production-ready mock APIs in seconds, simulating everything from success scenarios to network failures and timeouts. Accelerate your development cycle and ship with confidence. Get started for free with dotMock.

Get Started

Start mocking APIs in minutes.

Try Free Now

Newsletter

Get the latest API development tips and dotMock updates.

Stubs vs Mocks A Complete Developer Guide | dotMock | dotMock Blog