PYTHON ADVANCED: PROFESSIONAL ENGINEERING MASTERY / L09TESTING BEST PRACTICES: PYTEST AND TDD
课程 · 12 · 09 / 12
LESSON 09 · ADVANCED · 55 MIN · ◆ 3 INSTRUMENTS

Testing Best Practices: pytest and TDD

Master pytest with fixtures, parametrization, and mocking. Learn test-driven development, achieve meaningful coverage, and write maintainable tests.

TIP

Learning Objectives: After this lesson, you'll master pytest with fixtures, parametrization, and mocking, understand test-driven development, achieve meaningful coverage, and write maintainable tests.

Why Testing Matters

Tests are insurance for your code—they catch bugs before users do and give you confidence to refactor.

The Testing Pyramid

FIG. 02Flow Diagram
DIAGRAM
LOADING INSTRUMENT
Fig. 02Flow diagrams, timelines, and process visualizations

The pyramid says: write many cheap, fast tests at the bottom and few expensive, slow tests at the top. Each layer up costs more to write and maintain and runs slower, so you want progressively fewer of them.

LayerShareTestsCost & Speed
Unit~70%Individual functions/classes, isolated, external dependencies mockedFastest, cheapest
Integration~20%Component interactions with a real database or API callsMedium speed
End-to-End~10%Full user workflows via browser automation and real systemsSlowest, most expensive

Whatever the layer, good tests share five properties — the FIRST qualities:

  • Fast — run in milliseconds, so the whole suite stays quick.
  • Isolated — don't depend on other tests or on execution order.
  • Repeatable — produce the same result every time, on any machine.
  • Self-validating — pass or fail clearly, with no manual interpretation.
  • Timely — written close to the code they exercise (ideally first).

pytest Fundamentals

Basic Test Structure

FIG. 04Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 04Interactive Python code execution environment

Test Organization

FIG. 06Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 06Interactive Python code execution environment

Fixtures: Setup and Teardown

Fixtures provide reusable test setup and cleanup.

FIG. 08Flow Diagram
DIAGRAM
LOADING INSTRUMENT
Fig. 08Flow diagrams, timelines, and process visualizations

Basic Fixtures

FIG. 10Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 10Interactive Python code execution environment

Fixture Scopes

A fixture's scope controls how often it is created and torn down. A narrower scope is more isolated; a wider scope avoids repeating expensive setup. Choose the widest scope that keeps your tests independent.

ScopeCreatedUse it for
function (default)Once per test functionThe common case — most isolated
classOnce per test classState shared among a class's methods
moduleOnce per test fileExpensive setup reused across a file
sessionOnce per test sessionVery expensive operations, e.g. a DB connection

A wider-scoped fixture can be consumed by a narrower-scoped one. Here a module-scoped connection is opened once, while each test gets a fresh transaction that rolls back afterward:

@pytest.fixture(scope="module") def database_connection(): """Expensive: create once per module.""" conn = create_connection() yield conn conn.close() @pytest.fixture def transaction(database_connection): """Uses the module-scoped fixture.""" tx = database_connection.begin() yield tx tx.rollback()

The yield pattern. Code before yield is setup; the value yielded is what the test receives; code after yield is teardown that runs once the test completes — even if it fails. This is how you guarantee cleanup:

@pytest.fixture def temp_file(): """Create a temp file, clean it up after the test.""" import tempfile import os # Setup fd, path = tempfile.mkstemp() yield path # Teardown (runs after the test completes) os.close(fd) os.unlink(path) def test_write_to_file(temp_file): with open(temp_file, 'w') as f: f.write("test data") with open(temp_file) as f: assert f.read() == "test data" # temp_file is automatically cleaned up

Parametrization: Test Multiple Cases

FIG. 12Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 12Interactive Python code execution environment

Mocking: Isolate Units

FIG. 14Flow Diagram
DIAGRAM
LOADING INSTRUMENT
Fig. 14Flow diagrams, timelines, and process visualizations
FIG. 16Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 16Interactive Python code execution environment

Patching

FIG. 18Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 18Interactive Python code execution environment

Test-Driven Development (TDD)

FIG. 20Flow Diagram
DIAGRAM
LOADING INSTRUMENT
Fig. 20Flow diagrams, timelines, and process visualizations
TIP

▶ Try this first. Open the AlgorithmStepper below and step through the TDD loop one phase at a time — watch the status flip from RED (the test fails because the code doesn't exist) to GREEN (minimal code makes it pass) to REFACTOR (the code improves while tests stay green). Come back to the theory once you've seen the cycle move.

FIG. 22Algorithm Stepper
INTERACTIVE
LOADING INSTRUMENT
Fig. 22Step-through execution of algorithms and code

TDD inverts the usual order: you write the test before the code, then loop through three phases for every new piece of behavior.

  1. RED — write a failing test. Describe the new functionality as a test, run it, and watch it fail. The failure confirms the test is actually exercising something that doesn't exist yet.
  2. GREEN — make the test pass. Write the minimal code needed to go green. Don't over-engineer; just make it work.
  3. REFACTOR — improve the code. Clean up the implementation and remove duplication. The passing tests are your safety net that nothing broke.

Then repeat the loop for the next feature.

The example below applies this cycle to build a Stack: the tests are written first, then a minimal implementation makes them pass.

FIG. 24Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 24Interactive Python code execution environment

Testing Async Code

FIG. 26Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 26Interactive Python code execution environment

Coverage and Quality

Coverage tells you which lines your tests actually exercise. The standard tool is pytest-cov:

pip install pytest-cov pytest --cov=mypackage --cov-report=html

There are several kinds of coverage:

  • Line coverage — which lines were executed.
  • Branch coverage — which branches (each side of an if/else) were taken.
  • Function coverage — which functions were called.

Treat coverage as a guide, not a goal. 80%+ is healthy for most projects; chasing 100% is usually overkill. Spend your effort on the critical paths:

Worth testingSkip
Business logicThird-party libraries
Edge casesSimple getters/setters
Error handlingFramework code
Integration pointsConfiguration

A coverage report looks like this — Miss is the count of un-executed statements, and the tool points you at the exact missing lines:

Name Stmts Miss Cover -------------------------------------------- mypackage/__init__.py 5 0 100% mypackage/calculator.py 20 2 90% mypackage/utils.py 15 5 67% -------------------------------------------- TOTAL 40 7 82% Missing lines: calculator.py:45-46, utils.py:12-16

Best Practices

A handful of habits keep a test suite trustworthy and pleasant to work in:

  1. Name tests descriptively. Follow test_<what>_<condition>_<expected>, e.g. test_divide_by_zero_raises_error or test_empty_list_returns_none. The name should read like a spec.

  2. Structure each test as Arrange-Act-Assert (AAA). Set up inputs, perform the action, then assert on the result:

    def test_user_creation(): # Arrange name = "Alice" email = "alice@example.com" # Act user = create_user(name, email) # Assert assert user.name == name assert user.email == email
  3. Prefer one logical assertion per test. It makes failures easy to pinpoint. The exception is a small group of closely related assertions about the same outcome.

  4. Test the edge cases, not just the happy path: empty inputs, None values, boundary values, and large inputs.

  5. Test behavior, not implementation. Assert on the public interface, not private internals:

    # ✗ couples the test to internals assert len(user._internal_list) == 3 # ✓ tests observable behavior assert user.get_items() == [1, 2, 3]
  6. Use fixtures to stay DRY. Share setup across tests instead of repeating it.

  7. Keep tests fast. Mock slow operations, use in-memory databases, and parallelize with pytest -n auto.

  8. Keep tests independent. No shared state between tests; execution order must not matter.

Finally, a few pytest command-line flags worth memorizing:

pytest -v # Verbose output pytest -x # Stop on first failure pytest -k "pattern" # Run tests matching a name pattern pytest -m "marker" # Run marked tests pytest --lf # Run last failed pytest --ff # Run failed first pytest -n auto # Parallel execution (pytest-xdist) pytest --pdb # Drop into the debugger on failure

Practice Exercises

Exercise 1: Test a Shopping Cart

FIG. 28Python Code Executor
INTERACTIVE
LOADING INSTRUMENT
Fig. 28Interactive Python code execution environment

Key Takeaways

ConceptDescription
FixturesReusable test setup and teardown
ParametrizeRun same test with different data
MockReplace dependencies with fake objects
PatchTemporarily replace module attributes
TDDRed-Green-Refactor cycle
CoverageMeasure which code is tested
AAAArrange, Act, Assert pattern

Testing Checklist

  • Unit tests for business logic
  • Edge cases and error handling
  • Integration tests for components
  • Mocks for external dependencies
  • Async tests with pytest-asyncio
  • Coverage > 80% for critical code
  • Fast test suite (< 1 minute)
  • CI pipeline runs all tests

Next Steps

In the next lesson, we'll explore Design Patterns in Python—implement classic patterns like Factory, Strategy, Observer, and Dependency Injection the Pythonic way.


Ready to write elegant code? Design patterns await!


Further Reading

Official Docs

Tutorials & Talks

Modern Testing Stack

  • pytest-xdist — run your test suite in parallel on all CPU cores.
  • pytest-mock — friendlier API for unittest.mock.
  • pytest-cov — coverage plugin.
  • hypothesis — property-based testing. Finds bugs that example-based tests miss. Life-changing once you adopt it.
  • syrupy — modern snapshot testing.
  • pytest-asyncio — test async functions.

Mutation & Property-Based Testing

Books

  • Book: Python Testing with pytest (2nd ed., 2022) — Brian Okken. Definitive.
  • Book: Test-Driven Development with Python — Harry Percival (free online). TDD applied to a real Django app.
相关概念
pytesttestingtddmocking