Testing with pytest
🧪 Testing with pytest
pytest, the de facto standard testing tool in the Python ecosystem. This covers plain assert-based tests, fixtures for shared setup, parametrize for running one test against many inputs, and how pytest compares to Python's built-in unittest module.
✅ Why Test? A Quick assert Primer
assert is a plain Python keyword — if the condition is true, nothing happens; if it's false, it raises an AssertionError (optionally with a message). pytest builds its entire testing style around this one familiar keyword, rather than introducing a whole new assertion API.
🔬 Writing Tests with pytest
pytest auto-discovers test functions by naming convention alone: any file named test_*.py (or *_test.py), containing functions named test_*, is picked up automatically — no manual test registration, and no base class to inherit from, unlike unittest (see below).
🧩 Fixtures
A fixture is a function decorated with @pytest.fixture that provides shared setup for tests — any test function that takes a parameter matching the fixture's name automatically receives its return value. This is pytest's own dependency-injection style, avoiding the repeated setup code every test would otherwise need.
A fixture that uses yield instead of return gets a setup/teardown split — code before yield runs before the test, code after yield runs after, reusing the exact yield-pauses-and-resumes mechanic from Chapter 5's generator functions.
🔁 parametrize
@pytest.mark.parametrize runs the same test function once per tuple of inputs, reported as three separate, individually pass/fail test results — instead of writing three nearly-identical test functions by hand, or looping inside a single test where one failure would hide the others.
Floating-point numbers can't represent most decimal values exactly, so arithmetic that looks correct on paper often produces a result that's off by a tiny amount — this isn't a Python bug, it's how binary floating-point works in every language. pytest.approx() compares with a small allowed tolerance instead of exact equality, exactly for this situation.
pytest vs unittest
| unittest (standard library) | pytest (third-party) | |
|---|---|---|
| Test structure | Class-based, inheriting from TestCase | Plain functions — no class or inheritance required |
| Assertions | self.assertEqual(a, b), self.assertTrue(x), a whole API of methods to remember | Plain assert — one keyword covers everything |
| Setup/teardown | setUp()/tearDown() methods | Fixtures with @pytest.fixture, more flexible and reusable |
| Multiple inputs | Manual loops or repeated methods | @pytest.mark.parametrize |
unittest ships with Python itself, so it needs no installation — but pytest (a pip install away, per Course 1, Chapter 9) is the de facto standard in real-world Python projects for a reason: noticeably less boilerplate per test, and both fixtures and parametrize have no clean unittest equivalent.
assert
Plain Python keyword — raises AssertionError if the condition is false.
Test discovery
test_*.py files, test_* functions — found automatically, no registration.
Fixtures
@pytest.fixture — shared setup, injected by matching parameter name.
parametrize
Runs one test function against many input/expected-output pairs.
💻 Coding Challenges
Challenge 1: Test a String Function
Write a function is_palindrome(s) (reusing the logic from Course 1, Chapter 5's palindrome challenge), then write two pytest test functions for it: one asserting is_palindrome("racecar") is True, one asserting is_palindrome("python") is False.
Goal: Practice writing basic pytest test functions with plain assert.
Challenge 2: A Fixture for Shared Test Data
Write a @pytest.fixture called sample_scores that returns the list [85, 90, 78, 92]. Then write two tests that both use it — one asserting the average is 86.25, one asserting the max is 92.
Goal: Practice sharing setup data across multiple tests using a fixture instead of repeating the list in every test.
Challenge 3: Parametrized FizzBuzz Test
Write a fizzbuzz(n) function (reusing the logic idea from Course 1, Chapter 3's FizzBuzz-style challenge, but returning a string instead of printing), then use @pytest.mark.parametrize to test it against at least 4 different inputs and their expected outputs in a single test function.
Goal: Practice parametrize to cover multiple cases without writing a separate test function for each.
🎓 Course Complete!
That's all 8 chapters of Python Intermediate — OOP, inheritance and dunder methods, exception handling, file I/O, comprehensions and generators, decorators, working with data, and testing with pytest. Next up: Python Advanced (py3), starting with Advanced OOP.