Skip to main content

Small Rules, Big Difference

Good tests follow a few habits:

1. Clear names — test_greet_empty_name says what broke without opening the file.
2. One behavior per test — small tests point straight at the problem.
3. Tables for many cases — one parametrized test instead of copied tests.
4. Messages with got and want — a failure should explain itself.
5. Separate tests to see everything, several asserts in one test where continuing is pointless.
6. Run tests often — after every small change, not once at the end.
7. Tests live next to the code — same folder, test_ prefix.


Failure Messages That Help

A failing test is read in a moment of confusion — the message must carry the whole story: which call, what came back, what was expected.

A good shape is func(input) = got; want want. The !r conversion in an f-string prints strings with quotes, so a missing space or a missing comma is visible right away.

A message with the whole story

greet returns the wrong greeting. Our message shows the call, the got and the want, and pytest adds its own comparison of the two strings below it — the bug (wrong word, missing comma and exclamation mark) is visible in the output alone, without opening the code.

Prompt
Write a Python test file where a greet function returns the wrong greeting ('Hi Ana' instead of 'Hello, Ana!') and the assert message reports the call, the got value and the want value, using !r so the strings are quoted. Show the failing pytest -v output.
Output: complete Python program

One Behavior per Test

A test that checks five things has five reasons to fail — and its name can only describe one of them. Splitting by behavior keeps every test small and its name honest. It matters twice over here, because a failed assert stops its test: five checks in one test means you only ever see the first failure.

Here the normal greeting and the empty-name fallback are two behaviors, so they get two tests. When one fails, its name already tells you which behavior broke.

Two behaviors, two tests

test_greet_name checks the normal case, test_greet_empty the fallback. The verbose output lists them separately — a failure would point at exactly one behavior.

Prompt
Write a Python test file with a greet function that greets by name and falls back to 'Hello, stranger!' for an empty name. Write two separate focused tests, one per behavior, with names that say what they check. Show the passing pytest -v output with both tests listed.
Output: complete Python program

How Much Is Tested: Coverage

The pytest-cov plugin (installed with pip install pytest-cov) measures which part of your code the tests actually execute. Run pytest --cov=. and a table is added after the test run: how many statements each file has, how many the tests missed, and the percentage covered.

The number is a compass, not a goal: a low value shows untested paths — here the test only walks the mild branch, so the freezing and hot returns are never executed. 100% coverage does not prove the code is correct, but the uncovered lines are guaranteed unchecked.

Measure the coverage

describe has three branches, the test exercises one. The report counts 8 statements, 2 missed, 75% covered — the freezing and hot returns are the missed ones. Tests for the cold and the hot branch would drive the number up.

The whole file is measured, the test included, which is why the number is not lower: the test lines themselves all ran.

Prompt
Write a Python test file with a describe function that returns freezing, mild or hot depending on a temperature, and a single test that only checks the mild branch. Show the pytest --cov output with the statement counts and the coverage percentage.
Output: complete Python program
Measure the coverage