Learning Goals
3 min- Create a
Mockand set itsreturn_value. - Simulate exceptions with
side_effect. - Assert how the mock was called (
assert_called_with,call_count). - Know when to use
MagicMockvsMock.
Warm-Up · A Self-Configuring Fake
5 minfrom unittest.mock import Mock m = Mock() m.anything # works — returns another Mock m.foo() # works — returns a Mock m.bar(1, 2, x=3) # works — and remembers it was called like this print(m.bar.call_count) # 1 print(m.bar.call_args) # call(1, 2, x=3)
You don't hand-write fakes anymore. A Mock accepts any attribute access and any call without complaint, returns mocks by default, and records every interaction — so you can both feed it answers and verify how your code used it.
New Concept · Configuring & Asserting
14 minreturn_value — what the mock returns when called
from unittest.mock import Mock weather = Mock() weather.current_temp.return_value = 35 # configure the canned answer assert weather_advice(weather, "KL") == "hot!" assert weather.current_temp.return_value == 35
side_effect — raise errors or return a sequence
# simulate an exception m = Mock() m.fetch.side_effect = ConnectionError("offline") # calling m.fetch() now raises ConnectionError # return different values on successive calls m2 = Mock() m2.next.side_effect = [1, 2, 3] m2.next() # 1 m2.next() # 2 # side_effect can also be a function m3 = Mock() m3.double.side_effect = lambda x: x * 2 m3.double(5) # 10
Assert how it was called
m = Mock() m.save("Aisyah", age=13) m.save.assert_called() # was called at least once m.save.assert_called_once() # exactly once m.save.assert_called_with("Aisyah", age=13) # with these exact args print(m.save.call_count) # 1 m.save.assert_not_called() # would FAIL here
These assertions verify behaviour: "did my code call save with the right name?" — useful when the action is a side effect (saving, emailing) with no return value to check.
Mock vs MagicMock
Mock plain mock; does NOT support "magic" dunder methods
MagicMock a Mock that ALSO supports __len__, __iter__, __getitem__, etc.
→ use MagicMock when the code does len(x), x[0], for i in x, etc.from unittest.mock import MagicMock m = MagicMock() m.__len__.return_value = 3 print(len(m)) # 3 (a plain Mock would error here)
Rule of thumb: reach for MagicMock by default; it's a superset. (patch, next lesson, uses MagicMock automatically.)
Worked Example · Testing a Notifier
12 min# notify.py — sends an alert via an injected 'sender' def alert_if_low(balance, threshold, sender, user_email): if balance < threshold: sender.send(user_email, f"Low balance: RM{balance}") return True return False
# test_notify.py from unittest.mock import Mock from notify import alert_if_low def test_sends_when_below_threshold(): sender = Mock() result = alert_if_low(20, 50, sender, "a@x.com") assert result is True # verify the side effect happened with the right args sender.send.assert_called_once_with("a@x.com", "Low balance: RM20") def test_no_send_when_above_threshold(): sender = Mock() result = alert_if_low(100, 50, sender, "a@x.com") assert result is False sender.send.assert_not_called() # no email should go out def test_handles_sender_failure(): sender = Mock() sender.send.side_effect = ConnectionError("smtp down") import pytest with pytest.raises(ConnectionError): alert_if_low(20, 50, sender, "a@x.com")
$ pytest test_notify.py -v test_sends_when_below_threshold PASSED test_no_send_when_above_threshold PASSED test_handles_sender_failure PASSED 3 passed
Read the diff
No real emails sent! The Mock sender lets us verify three things: that an email was sent with the exact message when balance is low (assert_called_once_with), that it was not sent when balance is fine (assert_not_called), and that our code propagates a sender failure (side_effect). This is the canonical pattern for testing code with side effects.
Try It Yourself
13 minMock a dependency, set its return_value, and test a function that uses it. Assert the result.
Use side_effect = SomeError(...) to test that your code handles a dependency failure gracefully.
Test a function whose only job is a side effect (logging, saving). Use assert_called_once_with to verify the exact arguments, and assert_not_called on the no-op path.
Mini-Challenge · Retry Logic
8 minTest a function that retries a flaky call up to 3 times. Use side_effect as a list: [ConnectionError, ConnectionError, "success"] — fail twice, then succeed. Assert the result is "success" and that the call happened exactly 3 times.
Show one possible solution
from unittest.mock import Mock def fetch_with_retry(client, retries=3): for attempt in range(retries): try: return client.get() except ConnectionError: if attempt == retries - 1: raise # (loop handles the retry) def test_retries_then_succeeds(): client = Mock() client.get.side_effect = [ConnectionError, ConnectionError, "data"] assert fetch_with_retry(client) == "data" assert client.get.call_count == 3
Non-negotiables: a list side_effect to script the failure-then-success sequence, and a call_count assertion to prove the retry count.
Recap
3 minA Mock accepts any access/call and records it. Configure with return_value; simulate errors or sequences with side_effect; verify usage with assert_called_with / call_count / assert_not_called. Use MagicMock when the code needs dunder support (len, indexing, iteration). Next: injecting these without changing your code — the @patch decorator.
Vocabulary Card
- return_value
- What a mock returns when called.
- side_effect
- Raise an exception, return a sequence, or run a function on call.
- assert_called_with
- Verify the mock was called with specific arguments.
- MagicMock
- A Mock that also supports dunder methods (len, iter, getitem).
Homework
4 minWrite a function with an injected dependency that has a side effect (save, send, log). Test it three ways: happy path (verify the call), no-op path (assert_not_called), and failure path (side_effect raises). Bonus: a retry test using a list side_effect.
Model on test_notify.py + the retry mini-challenge. The three paths (sends / doesn't send / fails) cover the behaviour fully.