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([]) == 05. 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
-
Step 01
Arrange
Create representative inputs and dependencies.
-
Step 02
Act
Call the behavior under test.
-
Step 03
Assert
Check the documented outcome.
Code examples
def total(values: list[int]) -> int:
return sum(values)
def test_total_handles_empty_input() -> None:
assert total([]) == 0
The test passes when total([]) returns 0.
def normalize_name(name: str) -> str:
return name.strip().title()
def test_normalize_name() -> None:
assert normalize_name(' ada lovelace ') == 'Ada Lovelace'
The assertion passes for the expected normalized name.
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)
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
Your learning progress