Unit 14 · lesson
Automated Testing With `pytest`
Automated Testing With pytest
A pile of assert statements can prove ideas. A test framework turns those checks into a repeatable development tool.
This Unit uses pytest.
Keep the package tied to the project environment
If your workspace does not already provide pytest, install it into the selected project environment:
python -m pip install pytest
Then verify that the same Python environment can run it:
python -m pytest --version
Using python -m pytest keeps the command visibly attached to the current Python interpreter. That is useful when a computer has multiple environments.
Give pytest discoverable tests
A simple project might look like:
project/
├── my_math.py
└── tests/
└── test_my_math.py
my_math.py:
def multiply(a, b):
return a * b
tests/test_my_math.py:
from my_math import multiply
def test_multiply_positive_values():
assert multiply(3, 4) == 12
def test_multiply_by_zero():
assert multiply(5, 0) == 0
Run from the project root:
python -m pytest
Pytest uses naming/discovery conventions to locate tests. If it reports that no tests were collected, that is a different problem from a collected test failing.
Learn to distinguish:
NO TEST COLLECTED
≠
TEST COLLECTED AND FAILED
Read failure output as evidence
Break the implementation:
def multiply(a, b):
return a + b
A failed test should show which test failed and which values did not match.
Do not scroll past that output looking only for the red color. Extract:
TEST NAME
EXPECTED
ACTUAL
SOURCE LOCATION
That is the testing equivalent of reading a traceback.
Arrange, Act, Assert is a reading tool
Some tests become clearer when separated into three jobs:
def test_add_player_to_roster():
# Arrange
roster = []
# Act
add_player(roster, "Nova")
# Assert
assert roster == ["Nova"]
Turn a requirement into repeatable evidence
A focused test prepares a known state, exercises one behavior, then compares the actual result with the expected contract.
Drag nodes to inspect the relationships. Motion shows the active path; the plain background keeps attention on the relationships instead of graph-paper decoration.
View static diagram
You do not need comments saying Arrange, Act, and Assert in every tiny test. Use the pattern when it helps you see setup, behavior, and expectation separately.
Expected exceptions should be intentional
Suppose the contract says malformed email text should be rejected with ValueError.
import pytest
def test_extract_domain_rejects_missing_at():
with pytest.raises(ValueError):
extract_domain("invalid_email")
Then the implementation can make that contract explicit:
def extract_domain(email):
if "@" not in email:
raise ValueError("email must contain @")
_, domain = email.split("@", 1)
return domain
The important order is:
decide intended failure behavior
↓
encode the expectation
↓
implement / repair
Do not observe an accidental IndexError and then declare that accident to be the official contract merely because it is easy to test.
Several cases can describe one boundary
When the same behavior should be checked with several inputs, pytest can parameterize cases:
import pytest
@pytest.mark.parametrize(
("score", "expected"),
[
(91, "A"),
(90, "A"),
(89, "B"),
(80, "B"),
(79, "BELOW B"),
],
)
def test_classify_score_boundaries(score, expected):
assert classify_score(score) == expected
Parameterization is useful when it makes the contract easier to see. It is not a contest to compress every test into one function.
Tests should not depend on random leftovers
A reliable test starts from a known state.
This is fragile:
def test_saved_player_exists():
data = load_players("players.json")
assert data[0]["name"] == "Nova"
What created players.json? What if another test changed it? What if you ran this test by itself?
A better test controls its starting data or uses a temporary location supplied by the test environment.
At the beginner level, the rule is enough:
A test should create or receive the state it depends on instead of trusting leftovers from a previous run.
Test the contract, not implementation trivia
Suppose calculate_total() is refactored internally from a loop to sum() but the required result does not change.
A good behavioral test still passes:
assert calculate_total([10, 20, 30]) == 60
A brittle test that checks exactly how many internal loop iterations occurred may fail even though the public behavior is still correct.
Sometimes internal behavior matters. Usually, start by testing the contract visible to the caller.
Preserve a discovered bug
A powerful workflow is:
- reproduce a bug;
- write a test that fails because of that bug;
- repair the implementation;
- rerun the test;
- keep the test in the suite.
Diagrams open at a readable shape-aware scale. Zoom or expand when you need more detail.
Now the bug has become a regression test. Future changes can challenge the same behavior automatically.
Your test-run receipt
Before moving on, keep one test command and its output. You should be able to explain:
- how many tests were collected;
- which test failed before a repair;
- what expected/actual evidence appeared;
- what changed in the implementation; and
- why the repaired suite now supports the behavior claim.
Lesson 3 moves from writing tests to designing application code that is actually testable.
Vocabulary lab
Flip the idea, not just the card
Explain the term before you reveal the back. Then compare your explanation with the definition, example, and warning.
Read all terms without animation
- pytest
- A Python testing framework that discovers tests, executes them, and reports assertion or exception failures. Example: python -m pytest. Do not confuse it with: The Python interpreter alone.
- Test Discovery
- The process a test framework uses to locate test modules and test functions according to its conventions/configuration. Example: Finding functions whose names begin with test_ in test files. Do not confuse it with: A test being discovered and then failing.
- Arrange Act Assert
- A way to organize a test into starting state, behavior execution, and expected-result checking. Example: Create roster, call add_player, assert roster contents. Do not confuse it with: A requirement that every test contain three labeled comments.
- Expected Exception
- An intentionally specified failure type that is part of the behavior contract. Example: Malformed email text raises ValueError. Do not confuse it with: Any accidental exception the current implementation happens to throw.
- Test Isolation
- Keeping a test's required starting state controlled so other tests or previous runs do not determine its result. Example: Creating fresh input data for the test. Do not confuse it with: Trusting a leftover project file from an earlier run.