Skillversitas

PyTest Overview

Pytest is the de-facto standard testing framework for Python. It provides a single command (pytest) that discovers, runs, and reports tests for an entire project, and it builds on plain Python assert statements instead of a separate assertion API. The value over the standard library's unittest is the ergonomics: the bare assert gives detailed failure diffs (the assertion rewriting), the @pytest.fixture replaces the boilerplate setUp/tearDown, the auto-discovery needs no registration, and a large plugin ecosystem adds coverage, parallelism, async, and reporting. This chapter covers what pytest is and why it replaced unittest as the default, the execution model (the collection, the run, the report), the naming conventions the discovery relies on, and the shape of a pytest project.

Why Pytest

# the unittest way: the assertEqual + the setUp boilerplate
import unittest

class TestWordCount(unittest.TestCase):
    def setUp(self):
        self.text = "the quick brown fox"

    def test_word_count(self):
        from texttools import word_count
        self.assertEqual(word_count(self.text), 4)

# the pytest way: the bare assert + the plain function
# the SAME test, the no boilerplate, the failure diff is
# detailed (the expected vs the actual, the element that
# differs)
from texttools import word_count

def test_word_count():
    assert word_count("the quick brown fox") == 4

The comparison is the pitch. The unittest test needs the TestCase class, the setUp for the shared state, and the assertEqual method (the failure message is the two values). The pytest test is a plain function with a bare assert, and when the assert fails, pytest's assertion rewriting shows the detailed diff (the word_count(...) == 4 expanded to 3 == 4 with the actual value) - the failure is readable without re-running with a debugger. The second gain is the fixture: the shared state is the @pytest.fixture function (the declared once, the requested by name), which replaces the setUp/tearDown pair and scales to the session scope and the dependency graph. The third is the ecosystem: the coverage, the parallel run, the async, the HTML report, the mock - all are plugins that hook into the same conftest.py and the ini configuration. The pytest is the framework the rest of this tutorial builds on, and the texttools module (the word count, the palindrome, the caesar cipher, the letter frequency) is the running example the chapters test.

The Execution Model

# the model: the collect -> the run -> the report
# 1. the COLLECT: the pytest walks the filesystem, finds
#    the test_*.py files and the test_* functions (the
#    Test* classes), and builds the test items (the
#    collection tree, the shown with the --collect-only)
# 2. the RUN: the each test item runs in the session
#    (the setup - the fixtures - the call - the teardown)
#    the order is the file order, the function order
# 3. the REPORT: the summary (the passed, the failed,
#    the skipped, the xfailed) + the failure details
#    (the traceback, the captured output)

# the inspect the collection without running:
pytest --collect-only -q
# the count of the collected items (the sanity check
# the discovery works, the no test silently missed)

# the exit code: the 0 = the all passed, the 1 = the
# tests failed, the 2 = the collection error, the 5 =
# no tests collected (the CI's the signal)

The execution is three phases. The collection walks the filesystem from the rootdir, finds the test_*.py files (and the tests directory), and inside them the test_* functions and the Test* classes, building the tree of test items (the --collect-only prints it without running). The run executes each item in the session: the setup (the fixtures the test requests, in dependency order), the call (the test function), and the teardown (the fixture finalizers, in reverse) - the order is the file order then the definition order, and a test never depends on another test's side effect. The report is the summary (the passed, the failed, the skipped, the xfailed) plus the failure details (the traceback, the captured stdout, the assertion diff). The exit code is the CI's signal: the 0 the all-passed, the 1 the failures, the 2 the collection error, the 5 the no-tests-collected (the misconfiguration the pipeline must catch). The model is the reason the next chapters separate the discovery rules, the run behavior, and the report flags - the three phases are the three surfaces the framework exposes.

The Project Layout

# the LAYOUT: the source and the tests side by side
# (the package the tested, the tests/ the test files)
#
# texttools/
#   texttools/
#     __init__.py      - the word_count, the palindrome
#     cipher.py        - the caesar, the frequency
#   tests/
#     conftest.py      - the shared fixtures (the text
#                        the tests use, the sample data)
#     test_word.py     - the word_count's tests
#     test_palindrome.py
#     test_cipher.py
#   pyproject.toml     - the [tool.pytest.ini_options]
#
# the conftest.py is the discovery's anchor (the pytest
# uses it to find the rootdir and the import mode), and
# the pyproject's ini_options is the config (the testpaths,
# the markers, the addopts) the project shares

The layout is the source package beside the tests directory, and the conftest.py at the tests root is the anchor: the pytest finds the conftest to set the rootdir, to import the package under test (the import mode), and to define the shared fixtures (the sample text, the temp setup) the test files request. The pyproject.toml carries the [tool.pytest.ini_options] (the testpaths, the registered markers, the addopts like the -ra) so the configuration is versioned with the code. The rules that make the discovery work are the naming conventions (the test_*.py files, the test_* functions, the Test* classes without the __init__), and the next chapter installs pytest and runs the first test to see the model in action - the collection, the run, and the report, on the texttools module.

The Working Rules

  • The bare assert is the API. No assertEqual, no the TestCase - the assert is rewritten by pytest for the detailed diff, and the test is a plain function the discovery finds by the name.
  • The fixture replaces the setUp/tearDown. The @pytest.fixture is the declared setup (the yielded teardown), the requested by name, and the scoped from the function to the session - the shared state is the fixture, not the global.
  • The three phases are the model. The collect (the --collect-only the sanity check), the run (the setup/call/teardown, the file order), the report (the summary plus the failure diff) - and the exit code (the 0/1/2/5) is the CI's signal.
  • The conftest.py is the anchor, the pyproject is the config. The conftest at the tests root sets the rootdir and the shared fixture; the [tool.pytest.ini_options] versions the testpaths, the markers, and the addopts.

Summary

Pytest is the de-facto Python testing framework: the single pytest command that collects, runs, and reports the project's tests, built on the bare assert (the assertion rewriting gives the detailed failure diff) and the @pytest.fixture (the setup/teardown the unittest's setUp/tearDown boilerplate is replaced by). The execution is the three phases - the collection (the test_* files and functions, the --collect-only the inspect), the run (the per-item setup/call/teardown, the file order, the no cross-test dependency), and the report (the summary plus the failure details, the exit code the 0/1/2/5 the CI's signal). The layout is the package beside the tests directory, the conftest.py the discovery's anchor and the shared fixture, and the pyproject.toml the versioned configuration. The texttools module (the word count, the palindrome, the cipher, the frequency) is the running example, and the next chapters install the framework and build the test suite against it.