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"]
Interactive model

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 diagramStatic Arrange Act Assert test model

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:

  1. reproduce a bug;
  2. write a test that fails because of that bug;
  3. repair the implementation;
  4. rerun the test;
  5. keep the test in the suite.
Regression Visual
Regression Visual

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.

1 / 5
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.