Pytest Basics and Fixtures
Scenario
You have a massive test file with 100 lines of setup (database connection, user creation) copied and pasted at the top of every single test function. It's brittle and slow.
Mental model
Fixtures are dependency injection for your tests. Just like FastAPI's `Depends`, a test simply declares what it needs, and pytest figures out how to build it, pass it in, and clean it up.
Deep dive
Pytest is the standard testing framework for Python. Instead of subclassing `unittest.TestCase`, you write plain `assert` statements. Pytest introspects the assertions to provide rich failure diffs.
A `fixture` is a setup function decorated with `@pytest.fixture`. When a test function includes the fixture's name in its arguments, pytest executes the fixture first and injects the returned value.
Fixtures use `yield` instead of `return` to handle teardown. Everything before `yield` is setup; everything after `yield` runs after the test finishes, ensuring resources like database connections are closed even if the test fails.
Code examples
With Pytest Fixtures (Clean)
import pytest
@pytest.fixture
def db_session():
# Setup: runs before test
db = setup_test_db()
yield db
# Teardown: runs after test
teardown_test_db(db)
def test_create_user(db_session):
# db_session is injected automatically
user = create_user(db_session, 'alice')
assert user.name == 'alice'
The fixture handles the lifecycle. The test only contains the actual logic. Pytest guarantees the code after `yield` runs.
Common mistakes
- Forgetting to yield: Using `return` instead of `yield` in a fixture means you cannot write teardown code. The fixture will just exit, leaving database connections or files open.
Recall questions
- How does pytest know to pass a fixture into a test?
- How do you execute teardown code in a pytest fixture?
- What happens if a test fails when using a generator (yield) fixture?
Questions & answers
How would you share a database connection fixture across hundreds of tests without recreating the engine every time?
Use a fixture with `scope='session'` to create the engine once. Then, use a `scope='function'` fixture that connects to the engine, starts a transaction, `yield`s the session, and rolls back the transaction in the teardown so tests don't see each other's data.
If test A and test B both require a `user` fixture, does test B get the exact same user object?
By default, fixtures are function-scoped. Pytest will execute the fixture twice, so test B gets a freshly created user object, preventing test pollution.
Continue learning
Next: Parametrized Tests
Next: Mocking and Patching