Assertions & Test Structure
Clear test structure makes failures diagnosable in seconds. pytest's plain assert statements plus the arrange-act-assert pattern are the foundation of readable Python tests.
Recipe
def test_discount_applied():
# Arrange
price = Decimal("100.00")
# Act
result = apply_discount(price, pct=10)
# Assert
assert result == Decimal("90.00")When to reach for this:
- Tests are hard to read or debug
- Failures show unhelpful messages
- Reviewers cannot tell what behavior is under test
Working Example
from decimal import Decimal
import pytest
from myapp.billing import apply_discount, DiscountError
class TestApplyDiscount:
def test_ten_percent_off(self):
price = Decimal("50.00")
result = apply_discount(price, pct=10)
assert result == Decimal("45.00")
def test_zero_percent_unchanged(self):
price = Decimal("50.00")
assert apply_discount(price, pct=0) == price
def test_invalid_percent_raises(self):
with pytest.raises(DiscountError, match="pct must be 0-100"):
apply_discount(Decimal("10"), pct=-1)What this demonstrates:
- Arrange-act-assert flow in every test
- One behavior per test function
pytest.raisesfor exception testing with message match- Class grouping is optional (pytest collects
test_*methods)
Deep Dive
Assertion Introspection
pytest rewrites assert to show values on failure:
assert result == expected
# AssertionError: assert Decimal('45.00') == Decimal('50.00')Helpful Assertions
| Instead of | Use |
|---|---|
assert x == True | assert x |
assert len(items) == 0 | assert not items |
assert type(x) == str | assert isinstance(x, str) |
bare pytest.raises(Error) | pytest.raises(Error, match="...") |
Gotchas
- Multiple asserts per test - unclear which failed. Fix: one logical behavior per test; split cases.
- Assert without message on complex checks - opaque failure. Fix:
assert x in items, f"{x} not in {items}". - Testing implementation details - brittle tests. Fix: assert outputs and public behavior, not internal calls.
- No cleanup after arrange - state leaks between tests. Fix: use fixtures for setup/teardown.
- Comparing floats with == - flaky failures. Fix:
pytest.approx(3.14, rel=1e-6).
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| unittest assertions | stdlib-only | You want introspection |
| hypothesis | Property-based checks | Simple input/output |
| snapshot testing | Complex output structures | Pure logic units |
FAQs
What is arrange-act-assert?
Setup data (arrange), call the code under test (act), verify the result (assert).
Should I use test classes?
Optional. Classes group related tests; functions are fine for simple modules.
How do I assert exceptions?
with pytest.raises(ValueError): or pytest.raises(ValueError, func, arg).
How do I compare floats?
assert result == pytest.approx(3.14).
Can I use plain assert?
Yes. pytest enhances assert failures automatically. No need for self.assertEqual.
How long should a test be?
5-15 lines ideal. If longer, extract setup into fixtures.
Should test names describe behavior?
Yes. test_discount_over_100_raises not test_discount_3.
How do I assert warnings?
with pytest.warns(DeprecationWarning):.
What about asserting None?
assert result is None (identity), not == None.
How do I test approximate dicts?
Compare keys individually or use pytest.approx on numeric values.
Related
- pytest Setup - project configuration
- Fixtures - arrange step reuse
- Parametrization - multiple inputs
- Testing Best Practices - conventions
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.