0% complete

Testing, debugging, and concurrency · Lesson 1 of 3

Unit tests and testable program structure

Write focused tests for normal cases, boundaries, and failures.

Lesson

1. Concept and mental model

A test checks an observable behavior and protects it from accidental regression. A unit test should be fast, deterministic, and focused on one contract. Python’s standard library includes unittest; many teams also use pytest for concise fixtures and assertions. Keep production logic in functions that accept inputs and return results so tests do not need to launch a full command-line program or connect to real services.

2. How it works

Test representative normal cases, boundary values, and invalid inputs. For a collection-processing function, include empty input, duplicate values, and a malformed record. Assert meaningful outcomes rather than internal implementation details; tests coupled to private helper names often make harmless refactoring unnecessarily difficult. Use temporary directories for file tests and replace network boundaries with fakes or mocks.

3. Use it safely and effectively

A passing test suite does not prove every possible behavior is correct. Combine tests with review, static checks, and manual verification for user-facing workflows. Do not let mocks diverge from the real API contract: keep a few integration tests for boundaries when they can run safely and predictably. Name tests by behavior and make failure output explain what was expected. Practice by testing both a correct total and rejection of invalid input.

4. Syntax reference

def total(values: list[int]) -> int:
    return sum(values)

def test_total_handles_empty_input() -> None:
    assert total([]) == 0

5. Guided walkthrough

Keep the calculation in a function that accepts explicit input, then write a test for a normal case and an edge case. Run the focused test during development and the full suite before release; a passing test suite is evidence for the cases tested, not proof of every behavior.

6. Common mistakes to avoid

  • Testing only the implementation details instead of externally observable behavior.
  • Writing tests that depend on current time, network availability, or shared mutable state without controlling those dependencies.

Visual guide

A feedback loop for tests

  1. Step 01 Arrange

    Create representative inputs and dependencies.

  2. Step 02 Act

    Call the behavior under test.

  3. Step 03 Assert

    Check the documented outcome.

Code examples

Worked example
PYTHON
def total(values: list[int]) -> int:
    return sum(values)

def test_total_handles_empty_input() -> None:
    assert total([]) == 0
Expected output
The test passes when total([]) returns 0.
Test a deterministic helper
PYTHON
def normalize_name(name: str) -> str:
    return name.strip().title()

def test_normalize_name() -> None:
    assert normalize_name('  ada lovelace ') == 'Ada Lovelace'
Expected output
The assertion passes for the expected normalized name.
Test an error condition
PYTHON
import pytest

def divide(left: float, right: float) -> float:
    if right == 0:
        raise ValueError('right must not be zero')
    return left / right

def test_divide_by_zero() -> None:
    with pytest.raises(ValueError):
        divide(1, 0)
Expected output
pytest reports the expected ValueError behavior.

Key points

  • Write focused tests for normal cases, boundaries, and failures.
  • Prefer explicit, testable behavior over hidden side effects.
  • Validate assumptions at system boundaries and handle failure deliberately.

Check your understanding

Practice

What makes a unit test useful?

Your learning progress

Finish this lesson