Proving It Works, Not Just Believing It
Running a program and reading the output by eye works fine until it has more than one path through it. Write an assertion that checks the answer for you, and let it fail loudly the moment the code stops agreeing with itself.
Read first: Naming a Piece of Work
Running a program and reading the output by eye works fine until it has more than one path through it. Write an assertion that checks the answer for you, and let it fail loudly the moment the code stops agreeing with itself.
assert is the smallest test you can write
Eyeballing output scales fine for one function with one path through it. It stops scaling the moment a function has a branch, an edge case, or a second person changing it three months later — you cannot re-read fifty print statements by hand every time, and after the second or third time, nobody does. A test is that eyeballing, written down once and run automatically forever after.
$ >>> def add(a, b):$ ... return a + b$ ...$ >>> assert add(2, 3) == 5$ >>> assert add(2, 3) == 6$ Traceback (most recent call last): ...$ AssertionErrorassert condition does nothing at all if condition is true, and raises an AssertionError the instant it is false. That is the entire mechanism every testing tool in Python — including pytest — is ultimately built on top of.
pytest is what turns a scattering of asserts into a suite you run with one command. It looks for files named test_*.py and functions inside them named test_*, runs every one it finds, and reports which passed and which raised — no test runner of your own to write.
$ $ pytest$ test_largest.py::test_returns_the_maximum_value PASSED$ test_largest.py::test_empty_list_returns_none PASSED $ 2 passed in 0.01sThe arrange, act, assert shape
A test that mixes setup, the call being tested, and the check into one tangled block is hard to read back later. Most well-written tests fall into the same three-part shape, in the same order, every time.
- 1Arrange
Set up whatever the test needs — here, just the two numbers being added.
- 2Act
Call the one thing actually being tested, and nothing else.
- 3Assert
Check the result against exactly one expected answer.
def test_add_negative_numbers(): # Arrange a, b = -2, -3 # Act result = add(a, b) # Assert assert result == -5The comments are not required — the value of the shape is that it usually gives each test exactly one thing to prove. A test that will not fit cleanly into arrange, act, assert is often a sign it is quietly trying to check two behaviours at once, and would read more clearly split into two tests instead of one.
A test that only ever passes is not testing anything
A test is only proof of something if it is possible for it to fail. Check three cases below, each testing a small function for finding the largest number in a list — see which ones actually earn their place.
A real test. It picks a specific input, states the one correct answer, and would fail loudly if largest ever returned 3 or 1 instead.
Not a real test. Both sides call the exact same function on the exact same input, so this passes even if largest is completely broken — it is only checking that the function agrees with itself, not that it is correct.
A real test, and an important one — the empty-list case is exactly the kind of edge a normal run-and-eyeball check tends to skip entirely.
Case 3 is one example of a whole category worth writing on purpose: the empty collection, the single-item collection, the negative number, the duplicate value, the input right on the boundary of a condition. None of these show up if you only ever run the "normal" case by hand and read the output — they are precisely the inputs a human tester tends to skip, and precisely the ones most likely to break a real function later.
Naming a test after what it proves, not what it calls
def test_largest(): assert largest([3, 1, 4]) == 4 def test_returns_the_maximum_value(): assert largest([3, 1, 4]) == 4 def test_empty_list_returns_none(): assert largest([]) is Nonetest_largest says only which function is involved. test_returns_the_maximum_value says what the test actually proves — when it fails months later, the name alone tells you what broke, before you have read a single line of the assertion.
Fixtures set up the scene so a test doesn't have to
Several tests often need the same setup — the same sample data, the same open file, the same list to test against. Repeating that setup in every test function works, but a pytest.fixture lets you write it once and have pytest hand it to any test that asks for it.
import pytest @pytest.fixturedef sample_scores(): return [88, 92, 79, 95] def test_average_of_sample_scores(sample_scores): assert average(sample_scores) == 88.5sample_scores as a parameter name in test_average_of_sample_scores is not a coincidence — pytest matches a test's parameters against fixture names by that exact name, runs the matching fixture first, and passes in whatever it returned. It is a small piece of machinery, that is rarely useful until the same setup has already been copied into three or four tests.
What a green coverage number actually proves
A coverage tool reports the percentage of lines your test suite actually ran — 100% means every line executed at least once while the tests were running, nothing more specific than that.
Key takeaways
- assert condition does nothing if the condition is true, and raises AssertionError immediately if it's false.
- pytest discovers test_*.py files and test_* functions automatically and runs every one — no test runner of your own to write.
- The arrange, act, assert shape gives most tests exactly one thing to prove; a test that won't fit it is often testing two things at once.
- A test only proves something if it's possible for it to fail, and testing an edge case like an empty list catches what an eyeball check tends to miss.
- A green coverage number proves every line ran at least once — it never proves the test checked the line against the right answer.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.