Introduction to Mocking and Stubs: Isolating Your Unit Tests
Learn how to use mocking and stubs to isolate your code from external dependencies. Master dependency injection to build faster, reliable unit tests.

Previously in this course, we covered First Steps into Unit Testing: Automate Your Quality Assurance and learned how to Asserting Expected Outcomes: Mastering Software Testing Validation. Those lessons assumed your code was self-contained, but real-world systems rarely are. Today, we bridge that gap by learning to isolate our logic from external dependencies using stubs and mocks.
The Problem: Why Isolation Matters
A "unit" in unit testing should be exactly that: a single, isolated piece of logic. If your function talks to a database, hits an external API, or reads from the filesystem, it is no longer a unit test—it's an integration test.
When tests depend on external systems, they become:
- Slow: Network calls take orders of magnitude longer than memory operations.
- Flaky: If the API is down, your test fails even if your code is perfect.
- Non-Deterministic: A test that relies on "current time" or "random numbers" might pass today and fail tomorrow.
To solve this, we use test doubles—substitutes that act like the real thing but are controlled by your test suite.
Stubs vs. Mocks: The Difference
While the terms are often used interchangeably, there is a technical distinction you need to master:
- Stub: A minimal implementation that provides canned answers to calls made during the test. It "stubs out" the dependency so your code can run.
- Mock: A more sophisticated object that records how it was called (e.g., "was this function called exactly once with these specific arguments?").
| Feature | Stub | Mock |
|---|---|---|
| Purpose | Provide data to the system | Verify behavior/interaction |
| Verification | Indirect (via state change) | Direct (did the call happen?) |
| Complexity | Low | Higher |
Worked Example: Dependency Injection
To make our code testable, we must use dependency injection. Instead of a class "hard-coding" its dependency (like creating a new Database instance inside its constructor), we pass that dependency in.
Imagine an OrderProcessor that sends a notification.
PYTHON# The real dependency(slow, sends real emails) class EmailService: def send(self, message): # Imagine complex SMTP logic here print(f"Sending email: {message}") # The class we want to test class OrderProcessor: def __init__(self, email_service): self.email_service = email_service def process(self, order_id): # ... logic to process order ... self.email_service.send(f"Order {order_id} processed!")
To test OrderProcessor without sending real emails, we create a Stub:
PYTHON# The Stub: No network, no side effects class StubEmailService: def send(self, message): return True # Just return success def test_order_processor(): # Inject the stub stub = StubEmailService() processor = OrderProcessor(stub) # Act result = processor.process(123) # Assert assert result is None # Or whatever behavior you expect
Hands-on Exercise
In your current project, identify a class that currently calls an external service or a complex helper class.
- Create a "Stub" version of that class that returns hardcoded values.
- Refactor your main class to accept the dependency via its constructor.
- Write a test that injects the stub and verifies that the
processorcalculatemethod executes without error.
Common Pitfalls
- Over-mocking: Don't mock everything. If you mock the entire system, your tests will pass even if the real components don't work together. Only mock boundaries (APIs, databases, hardware).
- Testing Implementation Details: A good test verifies what the code does, not how it does it. If your test breaks every time you rename a private helper method, you are likely mocking too deep.
- Forgetting to verify: With mocks, if you expect a function to be called, ensure your test actually checks that it was called. A mock that isn't asserted against is just a silent stub.
FAQ
Q: When should I use a real dependency instead of a mock? A: Use the real thing if it's fast, in-memory, and deterministic. Never mock your own simple data objects or pure utility functions.
Q: Is dependency injection just for testing?
A: No. It also makes your code more modular and easier to swap out implementations (e.g., switching from a FileLogger to a CloudLogger) without changing your core business logic.
Recap
We've learned that isolation is the key to reliable testing. By using dependency injection, we can pass in stubs to satisfy our code's needs during testing, ensuring our test suite remains fast and decoupled from unstable external systems. You now have the tools to prevent your external dependencies from dictating your test suite's success.
Up next: Organizing Test Suites


