Going Deeper
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
Some code is hard to test because it reaches outside itself. It checks the clock, sends an email, calls a service, or waits for data you do not control. Deeper testing is about making those outside edges predictable, then using the results to improve your design.
Mock the edge, test your code
A dependency is another piece of code or service your code relies on, and a mock is a fake object used in a test so you can control that dependency and inspect how your code used it. You already know the shape: a test protects behavior. A mock lets you protect behavior without sending real emails, charging real money, or waiting for real clocks.
Think of a mock as a stunt double. The scene still tests your actor's lines and timing, but nobody has to jump off a real roof.
from unittest.mock import Mock
def send_receipt(player, email, total, email_sender):
message = f"{player}, your total is ${total:.2f}"
email_sender(email, message)
return "receipt queued"
fake_email_sender = Mock()
status = send_receipt("Mina", "mina@example.com", 42, fake_email_sender)
fake_email_sender.assert_called_once_with(
"mina@example.com",
"Mina, your total is $42.00",
)
print(status)
print("email calls:", fake_email_sender.call_count)receipt queuedemail calls: 1
Mock() comes from unittest.mock, so it is standard library. The mock acts like a callable object. When send_receipt() calls it, the mock records the call. Then assert_called_once_with() checks the exact arguments.
This is still a unit test idea. You are not testing the real email system. You are testing that your code asks the email system for the right thing.
Patch names carefully
Patching means temporarily replacing a name during a test, then putting the original name back. It is useful when the code calls a dependency directly instead of receiving it as an argument.
The key rule: patch the name your code looks up. If daily_offer() calls get_today_name(), patch that name where daily_offer() can see it.
from unittest.mock import patch
def get_today_name():
return "Tuesday"
def daily_offer(player):
if get_today_name() == "Friday":
return f"{player} gets free tea"
return f"{player} gets regular rewards"
with patch("__main__.get_today_name", return_value="Friday") as fake_today:
print(daily_offer("Ravi"))
fake_today.assert_called_once()
print(daily_offer("Ravi"))Ravi gets free teaRavi gets regular rewards
The patch only lives inside the with block. That matters. Tests should leave the room tidy for the next test, just like tearDown() did in unittest.
Try it - patch one outside answer
Run this once. Then change the patched mood from "calm" to "tired" and predict the first printed line. The second line proves the original helper comes back after the patch.
Measure coverage
Test coverage is a measurement of which lines or branches ran while your tests ran. It is not a score for quality. It is a map of places your tests did and did not visit.
A common setup uses the third-party coverage tool with a test runner such as pytest:
$ python -m coverage run -m pytest
============================= test session starts =============================
collected 4 items
test_checkout.py .... [100%]
============================== 4 passed in 0.04s ==============================Then the coverage report might look like this:
$ python -m coverage report
Name Stmts Miss Cover
-------------------------------------
checkout.py 18 2 89%
test_checkout.py 12 0 100%
-------------------------------------
TOTAL 30 2 93%The report says checkout.py has 18 statements, and 2 did not run during tests. That is useful evidence. It does not tell you whether the tested lines had strong assertions.
High coverage with weak tests is like visiting every room in a house but never checking whether the lights work. Use coverage to find blind spots, then write meaningful checks around promises, boundaries, and past bugs.
TDD: red, green, refactor
Test-driven development, or TDD, is a workflow where you write a failing test before the implementation, make it pass, then clean up the design while the test stays green.
The loop has three colors:
- Red: write a small test for behavior that does not work yet.
- Green: write the smallest code that makes the test pass.
- Refactor: improve names, shape, or duplication without changing behavior.
def test_gold_badge_starts_at_90():
assert badge_for_score(90) == "gold"At red, badge_for_score may not exist yet, so the test fails. At green, you write enough code to return "gold" for 90. At refactor, you make the function readable and handle the next case.
TDD is not magic. It is a steering wheel. The failing test says where you are trying to go, the passing test says you arrived, and refactoring lets you improve the route without losing the destination.
A taste of property-based testing
Property-based testing means testing a general rule across many generated examples, instead of listing every example by hand. Hypothesis is the popular third-party Python library for property-based testing.
You already know example-based tests:
def test_discount_never_goes_below_zero():
assert final_price(10, 3) >= 0Hypothesis lets you express a broader property:
from hypothesis import given, strategies as st
@given(price=st.integers(min_value=0, max_value=10_000))
def test_no_discount_makes_price_negative(price):
assert final_price(price, discount=5) >= 0Read that as: "Try many integer prices from 0 to 10,000, and this rule should always hold." If Hypothesis finds a failing input, it tries to reduce it to a small example you can understand.
Property-based testing is strongest when a function has a clear rule:
- A total should never be negative.
- Sorting should not lose any players.
- Encoding then decoding should give back the original text.
- Adding an item should increase a cart's length by one.
You do not need Hypothesis for every test. Keep example tests for named stories and boundary cases. Reach for property tests when a rule matters across a wide space of inputs.
Checkpoint
Answer all three to mark this lesson complete
Testing has now moved from checking answers to shaping design. Next, Section 17 opens a different kind of pressure: programs that do more than one thing at a time, where timing, waiting, and shared work make your tests and debugging habits even more valuable.