Mocks, Stubs & Fakes
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
| Double | What it does | Example |
|---|---|---|
| Dummy | Fills a parameter that the test does not use | Passing None as a logger |
| Stub | Returns fixed answers to calls | A gateway that always answers "SUCCESS" |
| Spy | Records how it was called, so you can check later | An SMS sender that remembers each message |
| Mock | A double with expectations about the calls it should receive | "Charge must be called once with ₹1,299" |
| Fake | A simple but working implementation | An 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:
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")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:
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))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.
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)Registered
Already registered
Stored: {'isha@example.com': 'Isha'}
Error path tested: database timed outThe 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_autospecorMock(spec=RealClass)so a mock refuses methods the real object does not have. A plainMockhappily accepts a typo such asgateway.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_wthraise anAttributeErrorin 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.
Mockwithreturn_valuestubs answers;assert_called_once_withandassert_not_calledcheck calls.patchreplaces a name temporarily; patch where the name is looked up.side_effectmakes 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
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)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?
A fake really works, but in a simpler way than the production dependency.
2. How do you make a Mock method raise an exception?
side_effect can raise an exception or return a series of values.
3. Code in shop.py does from payments import charge. What should you patch?
Patch the name where the code under test looks it up, which is inside shop.
4. What is the main risk of mocking everything?
Mocks can drift from reality, so keep some integration tests with real or sandbox services.
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
