EthicsComputerCourses
Join training +91 90532 10052

Mocks, Stubs & Fakes

Software Testing · Lesson 18 of 30 Intermediate 7 min read Updated

Real code talks to things that are slow, costly or unpredictable in a test: payment gateways, SMS services, databases, the system clock. You do not want every unit test to charge a real card or wait for a real server. Test doubles are stand-ins for those dependencies, the same way a stunt double stands in for an actor.

In this lesson you will learn the five kinds of test double, use Python's built-in unittest.mock to stub answers and check calls, replace the clock with patch, write a fake, and learn when mocking does more harm than good.

The five kinds of test double

DoubleWhat it doesExample
DummyFills a parameter that the test does not usePassing None as a logger
StubReturns fixed answers to callsA gateway that always answers "SUCCESS"
SpyRecords how it was called, so you can check laterAn SMS sender that remembers each message
MockA double with expectations about the calls it should receive"Charge must be called once with ₹1,299"
FakeA simple but working implementationAn in-memory database instead of MySQL

In everyday speech people call all of these "mocks", and Python's Mock object can act as a stub, spy or mock. The words still matter, because they tell you what the test is checking: a stub controls the inputs to your code, while a mock or spy checks the outputs your code sends to others.

You already used a hand-written stub in Test Levels. Python can create doubles for you.

Mock, return_value and call checks

unittest.mock.Mock creates an object that accepts any method call. Set return_value to decide what a method answers, then use assert methods such as assert_called_once_with to check how it was called:

PythonRun it yourself
from unittest.mock import Mock

def place_order(cart_total, phone, gateway, sms):
    result = gateway.charge(amount=cart_total, currency="INR")
    if result["status"] != "SUCCESS":
        return "Payment failed"
    sms.send(phone, f"Order confirmed. Paid Rs {cart_total}")
    return "Order placed"

# Stub: a gateway that always says SUCCESS
gateway = Mock()
gateway.charge.return_value = {"status": "SUCCESS"}
# Mock: an SMS sender we will check afterwards
sms = Mock()

print(place_order(1299, "9876543210", gateway, sms))
gateway.charge.assert_called_once_with(amount=1299, currency="INR")
sms.send.assert_called_once_with("9876543210", "Order confirmed. Paid Rs 1299")
print("charge calls:", gateway.charge.call_count, "| sms calls:", sms.send.call_count)

# Now the bank declines: no SMS must be sent
gateway.charge.return_value = {"status": "DECLINED"}
sms.reset_mock()
print(place_order(1299, "9876543210", gateway, sms))
sms.send.assert_not_called()
print("No SMS sent for a failed payment")
Output
Order placed
charge calls: 1 | sms calls: 1
Payment failed
No SMS sent for a failed payment

No money moved and no SMS was sent, yet both paths were tested in milliseconds. If place_order sent the SMS even after a declined payment, assert_not_called() would raise an AssertionError. Testing the "declined" path is the real win: with a real bank you could hardly make it happen on demand.

This design works because place_order receives its gateway and SMS sender as parameters. This is called dependency injection, and code written this way is much easier to test.

patch: replacing something your code imports

Sometimes code reaches for a dependency itself, such as the system clock. patch temporarily replaces a name for the duration of a with block or a test method, then puts the original back. A greeting that depends on the time of day can only be tested at 9 a.m., noon and 5 p.m. if you control the clock:

PythonRun it yourself
import time
import unittest
from unittest.mock import Mock, patch

def greeting():
    hour = time.localtime().tm_hour
    if hour < 12:
        return "Good morning"
    if hour < 17:
        return "Good afternoon"
    return "Good evening"

class TestGreeting(unittest.TestCase):
    def test_morning(self):
        with patch("time.localtime", return_value=Mock(tm_hour=9)):
            self.assertEqual(greeting(), "Good morning")

    def test_noon_is_afternoon(self):
        with patch("time.localtime", return_value=Mock(tm_hour=12)):
            self.assertEqual(greeting(), "Good afternoon")

    @patch("time.localtime", return_value=Mock(tm_hour=17))
    def test_five_pm_is_evening(self, fake_localtime):
        self.assertEqual(greeting(), "Good evening")
        fake_localtime.assert_called_once()

suite = unittest.TestLoader().loadTestsFromTestCase(TestGreeting)
unittest.TextTestRunner(verbosity=2).run(suite)
print("Real clock restored:", isinstance(time.localtime().tm_hour, int))
Output
test_five_pm_is_evening (__main__.TestGreeting.test_five_pm_is_evening) ... ok
test_morning (__main__.TestGreeting.test_morning) ... ok
test_noon_is_afternoon (__main__.TestGreeting.test_noon_is_afternoon) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.021s

OK
Real clock restored: True

As a decorator, @patch passes the replacement into the test as an extra argument, so you can inspect it. The golden rule of patch is: patch where the name is looked up. If shop.py does from payments import charge, you patch "shop.charge", not "payments.charge", because shop has its own copy of the name.

In pytest, the built-in monkeypatch fixture does the same job: monkeypatch.setattr("shop.charge", fake_charge). In Jest and Vitest you use jest.fn(), vi.fn(), jest.mock("./payments") or vi.mock("./payments.js").

side_effect and fakes

side_effect makes a mock raise an exception (or return a sequence of different values), which is how you test error handling. A fake is different: it is a small, real implementation that behaves correctly, such as an in-memory user store. Fakes are great when tests need several calls to work together.

PythonRun it yourself
from unittest.mock import Mock

class FakeUserRepo:
    """A working in-memory replacement for the real database."""
    def __init__(self):
        self.users = {}

    def save(self, email, name):
        if email in self.users:
            raise ValueError("email already registered")
        self.users[email] = name

    def exists(self, email):
        return email in self.users

def register(repo, email, name):
    email = email.strip().lower()
    if repo.exists(email):
        return "Already registered"
    repo.save(email, name)
    return "Registered"

repo = FakeUserRepo()
print(register(repo, "Isha@Example.com", "Isha"))
print(register(repo, " isha@example.com", "Isha again"))
print("Stored:", repo.users)

# side_effect makes a mock raise, to test error handling
flaky = Mock()
flaky.exists.side_effect = ConnectionError("database timed out")
try:
    register(flaky, "rohan@example.com", "Rohan")
except ConnectionError as error:
    print("Error path tested:", error)
Output
Registered
Already registered
Stored: {'isha@example.com': 'Isha'}
Error path tested: database timed out

The fake proved that the second registration with different capitals and spaces is detected as a duplicate. The mock with side_effect let us see what happens when the database times out; here the error escapes register, which may be a bug worth reporting ("should the user see a friendly message?").

When not to mock

Mocks are powerful, and overusing them is a common way to write tests that pass while the product is broken.

  • Do not mock the code under test. Mock its dependencies only.
  • Do not mock what you do not own without also testing the real thing. If your mock of the payment API is wrong, every test passes and production fails. Keep some integration tests against a sandbox.
  • Avoid checking every internal call. Tests that assert exact call sequences break whenever code is refactored, even when behaviour is unchanged. Prefer checking results.
  • Use create_autospec or Mock(spec=RealClass) so a mock refuses methods the real object does not have. A plain Mock happily accepts a typo such as gateway.chrage(...).

Common mistakes

  • Patching the wrong place. Patch the name where your code looks it up.
  • Forgetting reset_mock() when reusing one mock in several checks, so call counts add up.
  • A typo in an assert method. Misspelt helpers such as assert_called_once_wth raise an AttributeError in modern Python, but always make sure your test can actually fail.
  • Only mocked tests. A suite where every dependency is mocked never checks that the parts fit together.

Key points

  • Test doubles replace slow or unpredictable dependencies: dummies, stubs, spies, mocks and fakes.
  • Mock with return_value stubs answers; assert_called_once_with and assert_not_called check calls.
  • patch replaces a name temporarily; patch where the name is looked up.
  • side_effect makes a mock raise errors so you can test failure handling; fakes are small working implementations.
  • Mock dependencies, not the code under test, and keep some integration tests with the real services.

Exercise

Write a function send_otp(phone, sms, generator) that asks generator.new_code() for a code, sends it with sms.send(phone, f"Your OTP is {code}"), and returns the code. Test it with two Mock objects: make new_code return "482913", then check the return value and that sms.send was called once with the right text. Add a second check where sms.send has side_effect = TimeoutError("SMS gateway slow") and confirm the error is raised.

Show solution
Python
from unittest.mock import Mock

def send_otp(phone, sms, generator):
    code = generator.new_code()
    sms.send(phone, f"Your OTP is {code}")
    return code

generator = Mock()
generator.new_code.return_value = "482913"
sms = Mock()

assert send_otp("9812345678", sms, generator) == "482913"
sms.send.assert_called_once_with("9812345678", "Your OTP is 482913")
print("OTP sent correctly")

sms.send.side_effect = TimeoutError("SMS gateway slow")
try:
    send_otp("9812345678", sms, generator)
    print("FAIL: no error raised")
except TimeoutError as error:
    print("Timeout handled by caller:", error)
Output
OTP sent correctly
Timeout handled by caller: SMS gateway slow

The generator mock makes the OTP predictable, which a real random code never is. The second check shows that the function lets the timeout escape, so the screen that calls it must handle the error.

Quick Quiz

1. Which test double is a simple working implementation, such as an in-memory database?

2. How do you make a Mock method raise an exception?

3. Code in shop.py does from payments import charge. What should you patch?

4. What is the main risk of mocking everything?

Learn Software Testing with a mentor

Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call