Skillversitas

Assertions

The assertion is the check the test runs, and pytest's value is the rewriting: the bare assert a == b is compiled into a detailed self-explaining form, so the failure shows the sub-expressions (the a, the b, the a - b) instead of the bare assertion failed. The rewriter runs on the test module at import, it understands the ==, the in, the comparison, the boolean, and the container, and it skips what it cannot explain (the -p no:cacheprovider the off-switch, the --assert=plain the no-rewrite). This chapter covers the rewriting (the assert the diff, the -o inspect), the container checks (the list, the dict, the in), the exception checks (the pytest.raises context, the match), and the custom message - the assertion vocabulary the suite is built from.

The Rewriting: The Detailed Failure

# the BEFORE: the bare assert (the no detail, the
# the "assertion failed")
# the AFTER: the rewritten (the the sub-expression the
# the value, the the diff the the element)

from texttools import word_count

def test_word_count():
    result = word_count("the quick brown fox")
    assert result == 4
# the the failure (the the result is 3):
# >>>>> assert result == 4
# E       AssertionError: assert 3 == 4
# E        +  where 3 = word_count('the quick brown fox')

def test_list():
    words = ["the", "quick", "brown"]
    assert words == ["the", "quick", "brown", "fox"]
# the the container diff (the the element the the
# the missing, the the index the the first differ):
# E         at index 3: left has 3 items,
# E         right has 4 items (the the 'fox' the the
#         extra)

def test_in():
    assert "fox" in ["the", "quick", "brown"]
# the the "in" the the set/list (the the not-found the
# the detail: the the item + the the container's repr)

The rewriter is the assertion's power. The assert result == 4 that fails becomes the assert 3 == 4 with the where 3 = word_count('...') (the sub-expression evaluated and shown - the value AND its origin), and the container == becomes the element diff (the at index 3, the missing/extra item) instead of the whole-list repr the eye cannot parse. The in check shows the item and the container, the boolean chain (and/or) shows each side, and the </> the values. The --assert=plain disables the rewriting (the bare AssertionError, the C-extension the no-rewrite case), and the -p no:assertion the plugin off - the two are the debug's escape when the rewritten message is wrong (the custom __eq__ the misleading diff). The rule is the one: write the assert on the value (the result == 4, not the word_count(...) == 4 inline) so the where line names the sub-expression - the failure reads as the value and its source, and the next section's raises extends the same detail to the exception.

The Exception Check: pytest.raises

import pytest
from texttools import caesar_cipher

def test_cipher_empty_raises():
    # the raises context: the the exception EXPECTED,
    # the the body MUST raise (the the no-raise is the
    # the FAILED test, the the wrong-type is the the
    # the FAILED test)
    with pytest.raises(ValueError):
        caesar_cipher("", 3)

def test_cipher_raises_match():
    # the match: the the message CHECK (the the regex on
    # the the str(exception))
    with pytest.raises(ValueError, match="empty"):
        caesar_cipher("", 3)

def test_cipher_raises_value():
    # the the e: the the exception OBJECT (the
    # the assert on the the args / the the message)
    with pytest.raises(ValueError) as excinfo:
        caesar_cipher("", 3)
    assert "empty" in str(excinfo.value)

def test_no_bare_except():
    # the the ANTI-pattern: the the try/except the the
    # the no-raise SWALLOWED (the the test PASSES the
    # the no exception - the the false green)
    try:
        caesar_cipher("", 3)
    except ValueError:
        pass
    # the no assert - the the silent pass the
    # the the bug the raises the catch

The pytest.raises is the exception's assertion: the context manager requires the body to raise (the no-raise is the failed test, the wrong type is the failed test), and the match argument checks the message (the regex on the str(exception)) while the as excinfo hands the exception object to the further assert (the excinfo.value.args, the custom attribute). The anti-pattern is the bare try/except: pass: it passes when the exception does NOT raise (the swallow), which is the false green the raises context is the fix for - the context FAILS on the no-raise, the pass PASSES on it. The rule is the one: the expected exception is the with pytest.raises, the message is the match (the not the assert on the str alone), and the exception data is the excinfo.value - the three that cover the check the suite needs, and the custom message (the next section) is the assertion's label when the value alone does not explain the intent.

The Custom Message

def test_word_count_message():
    text = "the quick brown fox"
    result = word_count(text)
    # the message (the assert's label,
    # the failure the intent the the
    # the value the show):
    assert result == 4, (
        "the word_count must count the 4 words "
        "(got %d on %r)" % (result, text)
    )
    # the format: the comma + the the
    # the the string (the %-format the
    # the f-string the equivalent)

The custom message is the assert's comma argument: the assert condition, "the message" shows the message on the failure, and the message names the intent (the the word_count must count the 4 words) plus the actual value (the got 3 on '...') - the failure reads as the expectation and the reality, not just the 3 == 4. The rule is the use it when the value alone is opaque (the the domain quantity the number means nothing, the the message the meaning), and the skip it when the assert is self-explaining (the result == 4 the rewriter's where line is enough). The assertion vocabulary is now complete: the value check (the rewritten assert, the container diff), the membership (the in), the exception (the raises context, the match, the excinfo), and the label (the comma message) - the four that the next chapters' fixtures and parametrization apply to the texttools suite at scale.

The Working Rules

  • The bare assert is rewritten, the failure shows the sub-expression. The assert result == 4 becomes the assert 3 == 4 plus the where line, the container the element diff - the write the assert on the value (the named variable) so the rewriter's where names it, and the --assert=plain the off-switch.
  • The expected exception is the with pytest.raises, never the try/except: pass. The context FAILS on the no-raise (the pass PASSES on it - the false green), the match the message regex, and the as excinfo the exception object the further assert.
  • The in and the boolean are rewritten too. The "fox" in [...] shows the item and the container, the and/or each side - the membership and the logic get the same detail as the ==.
  • The comma message is the label, the value is the check. The assert cond, "the intent (got %d)" % v when the value alone is opaque, the skip when the rewriter's diff is self-explaining - the message the meaning, the assert the fact.

Summary

The assertion is the bare assert the rewriter compiles into the self-explaining form: the assert result == 4 the failure shows the assert 3 == 4 plus the where 3 = word_count('...') (the value and its origin), the container the element diff (the at index 3, the missing/extra item), and the in/and/or the same detail. The --assert=plain and the -p no:assertion are the off-switches. The exception check is the with pytest.raises context (the no-raise the FAILED test, the match the message regex, the as excinfo the object the further assert), and the try/except: pass the false-green anti-pattern it replaces. The custom message is the comma argument (the intent plus the actual, the assert cond, "the intent (got %d)" % v) for the value that alone does not explain. The four - the value, the membership, the exception, and the label - are the assertion vocabulary, and the next chapter's fixture turns the repeated setup into the declared dependency the assertion runs against.