Skip to main content

Module 04 - Testing and Quality

Reading time: ~10 minutes | Level: Intermediate → Engineering

Before reading further, predict what this test actually verifies:

def divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "error"

def test_divide():
result = divide(10, 2)
assert result # passes!
Show Answer

The test passes - but it verifies almost nothing useful.

assert result checks that result is truthy. The number 5.0 is truthy. But so is "error", any non-zero integer, any non-empty string, and almost every Python object.

This means the following would also pass:

def divide(a, b):
return "error" # always wrong - but the test still passes

def test_divide():
result = divide(10, 2)
assert result # "error" is truthy

The test contains no assertion about correctness. It only asserts existence. A test that cannot fail is not a test - it is a false sense of security embedded in your CI pipeline.

The correct assertions:

def test_divide():
assert divide(10, 2) == 5.0
assert divide(10, 0) == "error"

Now the test fails if the implementation returns the wrong value. That is the only kind of test that matters.

This is not a contrived example. Tests that assert truthiness instead of correctness exist in real production codebases. They accumulate quietly. They pass in CI. They give developers false confidence. And the first time a code change returns the wrong value, no test catches it.

That is the problem this module solves.

Why Testing Matters

Every bug has a cost. The cost depends entirely on where it is found.

Found duringRelative costWho finds it
Development (local machine)You
Code review2–5×A colleague
CI pipeline (test failure)5–10×The test suite
QA / staging environment10–50×The QA team
Production100×+Your users

These numbers come from decades of software engineering research. The finding is consistent across industries, languages, and team sizes: the later a bug is discovered, the more expensive it is to fix. A bug caught by a unit test takes minutes to diagnose and fix. A bug caught by a production incident takes hours of debugging, rollback planning, user communication, postmortem writing, and hardening follow-up.

Testing is not a quality tax. It is a cost-shifting strategy. You pay a small, predictable cost now - writing a test - to eliminate a large, unpredictable cost later.

Beyond cost, a well-maintained test suite delivers four concrete engineering advantages:

Refactoring confidence. A comprehensive test suite is what separates a refactoring from a gamble. Without tests, changing the internals of a module means hoping nothing broke. With tests, the suite tells you immediately if observable behaviour changed. This is what allows large Python codebases - Django, SQLAlchemy, pandas - to evolve without constant regressions.

Executable documentation. A test file is a runnable specification. It says: given these inputs, the system produces these outputs. Unlike a README, it cannot diverge from the actual code. If the implementation changes, the test fails. The test suite always describes what the code actually does, not what someone thought it did six months ago.

Design feedback. Code that is hard to test is often code with too many responsibilities, hidden dependencies, or unclear boundaries. The act of writing tests - especially writing tests before the implementation - surfaces these structural problems early, when they are cheap to fix.

CI/CD gate. In a modern engineering workflow, every commit triggers a pipeline. Tests are the gate. No test suite means no automated quality gate, which means every deployment is a manual risk assessment. The pipeline described in this module - linting, type checking, tests, coverage, pre-commit hooks - is the standard gate used by Python engineering teams at scale.

What This Module Covers

#LessonWhat It Covers
01unittestThe stdlib test framework - TestCase lifecycle, all assertion methods, assertRaises as context manager, setUp/tearDown/setUpClass/tearDownClass, unittest.mock.patch, TestSuite, TestLoader, skip/skipIf/expectedFailure, subtests with self.subTest
02pytestpytest at full depth - assertion rewriting via AST transformation, fixtures with scope (function/class/module/session), conftest.py, @pytest.mark.parametrize, built-in marks, pytest.raises/warns/approx, monkeypatch, capsys, tmp_path, essential plugins, pyproject.toml configuration
03Mockingunittest.mock in depth - Mock, MagicMock, patch, patch.object, side_effect, return_value, call_args, assert_called_with, where to patch (the import location, not the definition), spec and autospec, pytest-mock
04TDD PrinciplesRed–Green–Refactor cycle, writing tests before code, the three laws of TDD, test granularity, when TDD helps and when it creates overhead, integrating TDD into a real development workflow
05Code Coveragecoverage.py, branch coverage vs line coverage, pytest-cov, interpreting HTML reports, the coverage trap (100% line coverage does not mean no bugs), what to measure and what to legitimately exclude
06Linting and Formattingruff, flake8, pylint, black, isort, mypy for static type checking - configuration files, integrating into editors and CI pipelines, the distinction between style enforcement and correctness checking
07Pre-Commit HooksThe pre-commit framework - installing hooks, writing .pre-commit-config.yaml, running hooks in CI, composing a hook chain that enforces linting, formatting, type checking, and tests at every commit

Module Projects

ProjectCore Skills
Full Test Suite for Banking SystemWrite a complete test suite for a multi-class banking system: unit tests with pytest, mocking of external payment services, parametrised edge cases, fixtures for shared account state, branch coverage report above 90%, pyproject.toml configuration
CI Pipeline SetupConfigure a GitHub Actions workflow that runs ruff, mypy, pytest --cov, and pre-commit on every pull request - blocking merge on any check failure

Prerequisites

  • Python Foundation course complete (or equivalent depth)
  • Module 01 - Object-Oriented Programming (tests are written against classes and their public interfaces)
  • Module 02 - Functional Programming (decorators power @pytest.fixture and @pytest.mark.parametrize)
  • Module 03 - Python Internals (pytest assertion rewriting uses AST transformation; understanding inspect explains how fixture injection works by name)
  • Comfortable reading Python tracebacks

How to Use This Module

Lessons 01 and 02 cover the two primary testing frameworks. Read unittest first - it ships with Python and its explicit, class-based structure makes the mechanics of test lifecycle, setup, and teardown directly visible. Then read pytest - it is the industry standard for new Python projects, and understanding unittest's design makes pytest's choices clear and deliberate.

Lesson 03 (Mocking) extends both frameworks. Read it after you are comfortable with either.

Lesson 04 (TDD) is a methodology lesson. It does not introduce new tools - it changes how you use the tools you already know. Read it after Lessons 01–03.

Lessons 05, 06, and 07 build the tooling layer around your tests: measuring what your tests actually cover, enforcing code quality standards, and automating enforcement so that quality gates run on every commit without manual effort. Read them in order - they connect to each other and together describe a complete CI pipeline.

The opening puzzle of every lesson is not decorative. Attempt a genuine prediction before reading. These puzzles are chosen to expose specific assumptions that experienced Python developers carry - assumptions that produce tests that pass but catch nothing, mocks that verify the wrong call, and coverage reports that look complete but mask entire branches of logic.

After every lesson: run the examples yourself. Break them deliberately. A testing framework is something you understand by using it against real code, not by reading descriptions of it.

The Engineering Standard

In a professional Python engineering environment, every pull request runs through a CI pipeline. That pipeline runs ruff or flake8 for linting, black for formatting, mypy for type checking, and pytest --cov for tests and coverage. A PR that fails any of these checks cannot be merged.

This is not process overhead. It is the minimum quality gate that allows a team of engineers to work on the same codebase without introducing regressions, accumulating untested code, or spending sprint planning sessions triaging production incidents that a test would have caught in thirty seconds.

The seven lessons in this module give you every tool in that pipeline. By the end, you will know not just how to write a test, but how to write a test suite that a CI system can enforce, that covers the real failure modes of your code, and that gives you genuine confidence - not the false confidence of assert result - when you deploy.

Understanding testing at this depth also changes how you write code. Code designed with testability in mind has clear interfaces, explicit dependencies, and well-defined boundaries. It is code that is easier to read, easier to change, and easier to hand to the next engineer.

That is the standard this module is written to.

© 2026 EngineersOfAI. All rights reserved.