Skip to main content
Mocking and Stubbing

Mocking & Stubbing with Real-World Analogies: A Beginner’s Craft Guide

This guide is written for developers who have heard terms like "mocking" and "stubbing" but aren't sure what they mean in practice. We use concrete analogies from everyday life—like a restaurant kitchen or a car mechanic—to build a mental model you can apply immediately. By the end, you'll understand not just what these techniques are, but why they matter and how to use them effectively in your own tests.Why Mocking and Stubbing Matter: The Real ProblemImagine you're a chef preparing a multi-course meal. You can't wait for the fish delivery to arrive before you start chopping vegetables. Instead, you prepare a substitute—a piece of chicken you already have—to test your sauce recipe. In software testing, mocking and stubbing serve the same purpose: they let you test a piece of code without waiting for its real dependencies (databases, APIs, file systems) to be available. This is crucial because real dependencies introduce

This guide is written for developers who have heard terms like "mocking" and "stubbing" but aren't sure what they mean in practice. We use concrete analogies from everyday life—like a restaurant kitchen or a car mechanic—to build a mental model you can apply immediately. By the end, you'll understand not just what these techniques are, but why they matter and how to use them effectively in your own tests.

Why Mocking and Stubbing Matter: The Real Problem

Imagine you're a chef preparing a multi-course meal. You can't wait for the fish delivery to arrive before you start chopping vegetables. Instead, you prepare a substitute—a piece of chicken you already have—to test your sauce recipe. In software testing, mocking and stubbing serve the same purpose: they let you test a piece of code without waiting for its real dependencies (databases, APIs, file systems) to be available. This is crucial because real dependencies introduce uncertainty: the network might be down, the database might contain unexpected data, or the third-party API might have rate limits. Without mocking, your tests become slow, flaky, and hard to debug. They also become integration tests by accident, testing multiple components at once rather than isolating the unit you care about.

The Cost of Not Mocking

Consider a typical web application that calls an external payment gateway. If your test actually charges a credit card every time you run it, you'll quickly run up bills and likely violate terms of service. Even if you use a sandbox, the test depends on network connectivity and the sandbox's uptime. A single test suite might take 10 minutes instead of 2 seconds, and intermittent failures due to timeouts will erode your team's trust in the test suite. In my experience, teams that skip mocking often end up with tests that are rarely run or ignored when they fail, defeating their purpose entirely.

What This Guide Covers

We'll unpack the difference between stubs and mocks (they are not the same), show you how to decide when to use each, and walk through a step-by-step process for writing effective tests. We'll also compare three popular testing tools and discuss common mistakes—like over-mocking or mocking the wrong things—so you can avoid them. Whether you're working in JavaScript, Python, Java, or another language, the concepts are universal.

By understanding these fundamentals, you'll be able to write tests that are fast, reliable, and truly isolate the behavior you care about. Let's start by building a clear mental model using everyday analogies.

Core Concepts: Stubs vs. Mocks – The Kitchen and Mechanic Analogies

To understand the difference between stubs and mocks, let's use two analogies from everyday life. A stub is like a kitchen assistant who hands you a pre-prepared ingredient—say, a perfectly chopped onion—when you ask for it. You don't care how the onion was chopped; you just need it to be available so you can continue cooking your soup. In testing, a stub provides predefined data to the code under test. For example, if your function fetches a user's name from a database, you can stub the database call to always return "Alice" without actually querying the database. A stub replaces a real component with a simplified one that returns fixed values.

Mocks: The Car Mechanic Analogy

A mock is more like a car mechanic who not only hands you a part but also checks that you install it correctly. Suppose you ask the mechanic for a specific wrench. They give it to you, but they also watch to make sure you use it exactly three times in the right sequence. If you use it twice or in the wrong order, they'll tell you. In testing, a mock is an object that records how it was called—what arguments were passed, how many times, and in what order—and can verify those interactions later. This is useful when you need to ensure that your code calls a dependency in a specific way, such as sending an email exactly once after a user registers.

Key Differences at a Glance

  • Stub: Returns canned answers. Focus is on state—what data the code under test receives.
  • Mock: Records interactions and verifies behavior. Focus is on how the code under test uses its dependencies.
  • Spy: A hybrid that wraps a real object and records calls, often allowing the real implementation to run while also enabling verification.

When to Use Each

Use stubs when you only care about the data flowing into your code. For instance, if you're testing a function that calculates a discount based on a user's membership tier, you can stub the tier lookup to return "gold" and check the discount percentage. Use mocks when the correctness of the interaction is important—like verifying that a notification service is called after a purchase, or that the database save method is invoked with the correct parameters. In practice, many test frameworks blur the line, but understanding the conceptual difference will help you write clearer tests.

Let's look at a concrete code example. Suppose you have a function sendWelcomeEmail(userId) that fetches the user's email from a database and then calls an email service. In a test, you would stub the database call to return a known email address, and mock the email service to verify it's called with that address. The stub provides the input; the mock verifies the output action.

This distinction becomes even more important as your codebase grows. Overusing mocks can lead to brittle tests that break when you refactor internal implementation details. Overusing stubs can hide cases where your code never actually calls a dependency. A balanced approach is to use stubs for queries (read operations) and mocks for commands (write operations).

We'll revisit this balance later, but first, let's walk through a practical workflow.

Step-by-Step Workflow: How to Mock and Stub Effectively

Now that you understand the concepts, let's walk through a repeatable process for introducing mocks and stubs into your tests. We'll use a simple example: a function that calculates a user's total order price including a discount.

Step 1: Identify the Unit Under Test

The unit under test (UUT) is the smallest testable piece of your code — typically a single function or method. In our example, the UUT is calculateTotal(userId, orderItems). It takes a user ID and a list of items, looks up the user's discount tier from a database, applies the discount to the total price, and returns the result. The dependencies are: a database query to get the user's tier, and possibly a pricing service to compute item costs.

Step 2: Decide What to Replace

For the database query, we want to replace it with a stub because we only care about the data it returns (the discount percentage), not how it's called. For the pricing service, we might also stub it if it's an external API, but if it's an internal calculation, we could let it run. The key is to replace anything that makes the test slow, non-deterministic, or difficult to set up.

Step 3: Set Up the Stubs and Mocks

Using a framework like Python's unittest.mock, we can create a stub for the database call. For example: mock_db.get_discount_tier.return_value = 'gold'. This ensures that when the UUT calls get_discount_tier('user123'), it receives 'gold'. For the pricing service, if we need to verify it's called with the right arguments, we create a mock: mock_pricing.calculate.return_value = 100. Then we assert that it was called with the correct item list.

Step 4: Execute the Test

Run the UUT with the injected stubs and mocks. Assert on the returned value (e.g., assert result == 90 for a 10% discount on $100). Additionally, assert on mock interactions if needed: mock_pricing.calculate.assert_called_once_with(orderItems).

Step 5: Clean Up

Most testing frameworks automatically clean up after each test, but be careful with global state or singletons. In frameworks like Mockito, mocks are reset between tests by default. In others, you might need to call mock.reset() or use a setup/teardown pattern.

Practical Tips

  • Start simple: stub only one dependency at first, then add mocks as needed.
  • Use descriptive variable names for your mocks, like mock_email_service, to keep tests readable.
  • Don't mock everything—leave simple, deterministic functions real.

Let's now look at the tools that can help you execute this workflow.

Tools of the Trade: Comparing Mockito, Sinon, and unittest.mock

Your choice of mocking framework depends on your programming language and project needs. Here we compare three widely used tools: Mockito (Java), Sinon (JavaScript), and unittest.mock (Python).

FeatureMockitoSinonunittest.mock
LanguageJavaJavaScriptPython
Stubbingwhen(mock.method()).thenReturn(value)stub.returns(value)mock.return_value = value
Verificationverify(mock).method(args)sinon.assert.calledWith(spy, args)mock.assert_called_with(args)
Spying@Spy annotationsinon.spy(obj, 'method')mock.Mock(wraps=obj)
Partial Mocking@Spy or mock(CallRealMethod)stub.callThrough()mock.Mock(side_effect=...) with wraps
Async SupportVia CompletableFuture or Answerstub.resolves(value)mock(return_value=awaitable)
Community / MaturityVery mature, widely used in SpringMature, standard in Node.js test stacksBuilt-in, very mature

When to Choose Each

If you're in the Java ecosystem, Mockito is the de facto standard, especially with Spring Boot's test support. Its fluent API and integration with JUnit make it a pleasure to use. For JavaScript developers, Sinon is a lightweight, flexible library that works well with any test runner (Mocha, Jest, Vitest). It also provides fake timers and XHR, which are helpful for frontend tests. Python developers are fortunate to have a powerful mocking library built into the standard library, reducing dependencies. unittest.mock is feature-rich and supports almost any testing scenario, though its syntax can be verbose compared to third-party alternatives like pytest-mock (which wraps it more cleanly).

Economics and Maintenance Realities

All three tools are open source and free, but they impose a learning curve. Investing time in learning the API pays off: well-mocked tests run in milliseconds, catch bugs before deployment, and serve as living documentation. However, over-reliance on mocks can lead to tests that pass even when the real system fails—a phenomenon known as "false positives." To mitigate this, many teams adopt a test pyramid strategy: use unit tests with mocks for the bulk of tests, integration tests for critical paths, and end-to-end tests sparingly. This balances speed and realism. Also, keep your mocks simple; if a mock setup becomes too complex, consider refactoring the code under test to be more testable.

Let's now explore how to grow your testing practice within a team.

Growing Your Testing Practice: From Solo to Team-Wide Adoption

Adopting mocking and stubbing across a team requires more than just technical knowledge. It's a cultural shift that needs buy-in from developers, testers, and managers. Start small: introduce mock objects in a single module where tests are currently slow or flaky. Once the team sees the improvement—faster feedback, fewer flaky failures—they'll be more open to expanding the practice.

Pair Programming and Code Reviews

Pair programming is an excellent way to spread mocking skills. Work with a colleague to write a test for a function that calls an external API. Show them how to set up a stub for the API response and a mock to verify the call. During code reviews, encourage reviewers to check for proper mocking: are the tests isolating the unit? Are they verifying the right interactions? Over time, the team will develop a shared vocabulary and set of patterns.

Creating Test Templates

To reduce friction, create templates or example tests in your repository. For instance, a template for a service class might include stub setup for database calls and mock verification for external notifications. This lowers the barrier for developers new to mocking. Also, document your team's conventions: when to stub vs. mock, how to name mock objects, and what to avoid (e.g., mocking third-party libraries directly—instead, wrap them in an interface you control).

Measuring Success

Track metrics that matter: test execution time, flaky test rate, and code coverage of critical paths. A successful mocking strategy should reduce test execution time by at least 50% (from minutes to seconds) and cut flaky failures significantly. Share these wins in team demos to reinforce the value. However, avoid vanity metrics like overall code coverage; 100% coverage with poor tests is worse than 70% coverage with meaningful tests.

Dealing with Resistance

Some developers feel that mocking introduces indirection and makes tests harder to read. Address this by showing that well-written mock tests are actually simpler than integration tests that require complex setup. Also, emphasize that mocking doesn't replace integration tests—it complements them. Use the analogy of a unit test being a quick health check, while integration tests are full physicals. Both are needed, but you run the quick check more often.

Now let's turn to the common pitfalls that can undermine your efforts.

Common Pitfalls and How to Avoid Them

Even experienced developers fall into traps when using mocks and stubs. Being aware of these pitfalls will save you hours of debugging.

Pitfall 1: Over-Mocking

Mocking everything in sight leads to brittle tests that break on any refactor, even when the behavior remains correct. For example, if you mock a simple utility function like formatCurrency(), a change in how that function is called (say, renaming a parameter) will cause the test to fail even if the overall logic is unchanged. Mitigation: Only mock external dependencies or components that are slow, non-deterministic, or unavailable in test environments. Leave pure functions and simple calculations unmocked.

Pitfall 2: Mocking the System Under Test (SUT)

A classic mistake is mocking the class you are actually testing. This happens when developers try to avoid instantiating real objects. But if you mock the SUT, you're not testing your code—you're testing the mock. Mitigation: Always test real instances of the class under test. Inject dependencies as mocks, never the SUT itself.

Pitfall 3: Using Mocks When Stubs Would Suffice

If you only need to provide data, use a stub. Using a mock adds unnecessary complexity and verification that can become stale. For instance, verifying that a getter method was called is usually pointless; you care about the returned value, not the interaction. Mitigation: Default to stubs for queries; only use mocks for commands or when interaction order matters.

Pitfall 4: Ignoring Async Behavior

Asynchronous code introduces subtleties: mocks may return promises or futures, and you need to await them correctly. Failing to do so can result in tests that pass even when the code is broken. Mitigation: Use your framework's async support (e.g., resolves in Sinon, side_effect with coroutines in Python) and always await or complete the async operation in your test.

Pitfall 5: Not Cleaning Mocks Between Tests

If mocks retain state across tests (e.g., call counts), you can get false failures or passes. Most frameworks clear mocks automatically per test, but if you use shared mocks in a test suite, reset them manually. Mitigation: Use fresh mocks per test, or call reset() in setup.

Avoiding these pitfalls will keep your test suite reliable and maintainable. Next, let's address common questions newcomers have.

Mini-FAQ: Answering Your Most Common Questions

Q: Do I need to mock every time I write a unit test?
A: No. Only mock dependencies that are external (network, database, filesystem) or that make the test slow or non-deterministic. Simple pure functions can be tested directly.

Q: What's the difference between a mock, a stub, and a fake?
A: A stub provides canned answers. A mock records and verifies interactions. A fake is a lightweight implementation that works like the real thing but is simpler (e.g., an in-memory database). Fakes are great for integration tests but too heavy for unit tests.

Q: Can I mock a private method?
A: Generally, you should not mock private methods because they are implementation details. Instead, test them indirectly through public methods. If a private method is complex enough to need mocking, consider extracting it into a separate class and testing it through its public interface.

Q: My mock isn't returning the expected value. What could be wrong?
A: Common causes: the mock is not being injected into the SUT (check your dependency injection), the method signature doesn't match, or the mock is being overridden elsewhere. Use your framework's debug features or print the mock's call args to diagnose.

Q: How do I mock a class constructor?
A: In Python, you can patch the class itself: with patch('module.ClassName') as mock_class:. Mockito allows mocking the constructor via mockConstruction. Sinon can stub the constructor with sinon.stub(obj, 'constructor'). The approach varies by language.

Q: Is it okay to mock a database call in a unit test?
A: Yes, that's a primary use case. However, also write an integration test that actually hits a test database to ensure your SQL queries are correct. The unit test checks your logic; the integration check checks your query syntax and schema assumptions.

Q: Aren't mocks just for test-driven development (TDD)?
A: No, mocks are useful regardless of when you write tests. In TDD, you write the test first and then implement code to pass it, which naturally encourages testable design. But you can also add mocks to existing tests to improve speed and isolation.

Q: How many mocks is too many?
A: A rule of thumb: if a test has more than three mocks, consider whether the SUT is doing too much. Refactor the SUT into smaller, focused units. Each test should ideally verify one behavior.

Let's wrap up with a synthesis of the key lessons and your next steps.

Synthesis and Next Actions: Your Journey Forward

We've covered a lot of ground: from the fundamental problem mocking solves, to the conceptual distinction between stubs and mocks, to a step-by-step workflow, tool comparisons, team adoption strategies, pitfalls, and a FAQ. Now it's time to put this knowledge into practice.

Action 1: Pick a Small Module

Choose a small, isolated feature in your current codebase that depends on an external service (e.g., sending emails, fetching weather data). Write a unit test for it using stubs and mocks. Aim for a test that runs in under a second and doesn't require network access.

Action 2: Refactor a Flaky Test

Identify a test that fails intermittently due to external dependencies. Introduce mocking to stabilize it. Measure the improvement in test reliability and speed. Share the results with your team.

Action 3: Hold a Knowledge-Share Session

Teach one colleague how to mock a dependency using your framework of choice. Pair-program on a test. Teaching solidifies your own understanding and builds team capability.

Action 4: Review Your Test Suite for Over-Mocking

Examine existing tests. Are there any that mock simple utility functions? Replace those mocks with real implementations. Check for tests that mock the SUT—fix them immediately. This will make your test suite more robust.

Action 5: Read Your Framework's Documentation

Each mocking framework has advanced features (argument matchers, partial mocks, custom answers). Spend an hour exploring these. You'll discover ways to simplify your tests.

Remember, the goal of mocking and stubbing is to write tests that are fast, reliable, and focused on your code's logic. They are tools, not ends in themselves. Use them wisely, and they will serve you well throughout your career.

Thank you for reading this guide. We hope the analogies and practical steps have demystified mocking and stubbing for you. Now go write some great tests!

About the Author

This article was prepared by the editorial team for this publication. We focus on practical explanations and update articles when major practices change.

Last reviewed: May 2026

Share this article:

Comments (0)

No comments yet. Be the first to comment!