Writing Concise Tests with Plain assert
Assertion introspection
Section titled “Assertion introspection”pytest rewrites asserts to show helpful diffs.
def test_username_format():
username = "alice_01"
assert username.startswith("alice")
assert " " not in usernameIf a check fails, pytest shows:
- the values involved
- a readable explanation
Testing exceptions
Section titled “Testing exceptions”import pytest
def parse_age(s: str) -> int:
age = int(s)
if age < 0:
raise ValueError("age must be >= 0")
return age
def test_parse_age_negative():
with pytest.raises(ValueError):
parse_age("-1")Prefer many small asserts over one huge assert.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Write a unittest TestCase
Section titled “Exercise 1 – Write a unittest TestCase”Exercise 2 – assertRaises
Section titled “Exercise 2 – assertRaises”Exercise 3 – setUp and tearDown
Section titled “Exercise 3 – setUp and tearDown”One statement, no vocabulary to learn
Section titled “One statement, no vocabulary to learn”def test_tax():
assert price_with_tax(100) == 120.0
def test_in():
assert "cat" in "concatenate"
def test_almost():
assert price_with_tax(19.99) == 23.99Measured failure output:
> assert price_with_tax(100) == 121.0
E assert 120.0 == 121.0
E + where 120.0 = price_with_tax(100)The + where line is the part worth noticing: pytest reports not only the comparison but
the sub-expressions that produced it, so you often do not need to open the file.
Testing exceptions
Section titled “Testing exceptions”import pytest
def test_negative_raises():
with pytest.raises(ValueError, match="must not be negative"):
price_with_tax(-1)match= takes a regular expression and is searched against the message. Without it, the
test passes for any ValueError — including one raised for a completely different
reason later.
def test_message_details():
with pytest.raises(ValueError) as exc:
price_with_tax(-1)
assert "negative" in str(exc.value)Parametrize instead of looping
Section titled “Parametrize instead of looping”@pytest.mark.parametrize("price,rate,want", [
(100, 0.2, 120.0),
(50, 0.1, 55.0),
(0, 0.2, 0.0),
(19.99, 0.2, 23.99),
])
def test_tax(price, rate, want):
assert price_with_tax(price, rate) == wantFour independent tests, each named and reported separately. A loop inside one test stops at the first failure and hides the rest; parametrize runs them all and tells you exactly which inputs broke.
flowchart TD L["a for loop inside one test"] --> S["first failure raises"] S --> H["remaining cases never run"] P["@parametrize"] --> N["one test per case"] N --> A["all cases run, each named"]
Floats
Section titled “Floats”assert 0.1 + 0.2 == pytest.approx(0.3)
assert result == pytest.approx(expected, rel=1e-3)pytest.approx is the equivalent of assertAlmostEqual, and it works inside lists and
dicts too — assert [0.1 + 0.2] == pytest.approx([0.3]).
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
What does the line starting with + where add to a pytest failure?
Reporting the intermediate value is often enough to diagnose the failure without opening the file.
pch.quizShowAnswer
B — the value of a sub-expression, such as where 120.0 = price_with_tax(100) — Reporting the intermediate value is often enough to diagnose the failure without opening the file.
-
Why prefer @parametrize over a for loop inside one test?
Each parametrized case is named independently, so a failure identifies exactly which inputs broke.
pch.quizShowAnswer
B — the loop stops at the first failing case, while parametrize runs every case as its own test and reports all failures in one run — Each parametrized case is named independently, so a failure identifies exactly which inputs broke.
-
What is wrong with pytest.raises(ValueError) without match=?
match= takes a regular expression searched against the message, which keeps the test tied to the failure it was written for.
pch.quizShowAnswer
B — it passes for any ValueError, including one raised later for an entirely different reason — match= takes a regular expression searched against the message, which keeps the test tied to the failure it was written for.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading