pytest Fixtures¶
Use pytest fixtures for setup/teardown and parameterized test data.
Mental Model
A fixture is a reusable setup recipe. You define it once as a function decorated with @pytest.fixture, and any test that names it as a parameter automatically receives its return value. Fixtures replace the setUp/tearDown ceremony of unittest with explicit dependency injection — each test declares exactly what it needs, nothing more.
Creating and Using Fixtures¶
Define reusable test setup with fixtures.
```python import pytest
@pytest.fixture def sample_data(): return {"users": ["Alice", "Bob"], "count": 2}
@pytest.fixture def temp_file(tmp_path): file = tmp_path / "test.txt" file.write_text("test content") return file
def test_data_access(sample_data): assert len(sample_data["users"]) == 2 assert sample_data["count"] == 2
def test_file_exists(temp_file): assert temp_file.exists() assert "test" in temp_file.read_text()
print("Fixtures can provide test data and resources") ```
Fixtures can provide test data and resources
Fixture Scopes¶
Control when fixtures are created and destroyed.
```python import pytest
@pytest.fixture(scope="function") # Default, run for each test def function_scope(): return "function"
@pytest.fixture(scope="class") # Shared per test class def class_scope(): return "class"
@pytest.fixture(scope="module") # Shared per module def module_scope(): return "module"
@pytest.fixture(scope="session") # Shared for entire session def session_scope(): return "session"
def test_scopes(function_scope): assert function_scope == "function"
print("Fixture scopes control resource lifecycle") ```
Fixture scopes control resource lifecycle
Exercises¶
Exercise 1. Write a pytest fixture sample_dict that returns {"name": "Alice", "age": 30}. Use it in two tests: one that checks the name key and one that checks the age key.
Solution to Exercise 1
```python import pytest
@pytest.fixture def sample_dict(): return {"name": "Alice", "age": 30}
def test_name(sample_dict): assert sample_dict["name"] == "Alice"
def test_age(sample_dict): assert sample_dict["age"] == 30 ```
Exercise 2. Create a fixture temp_file that creates a temporary file with some content, yields its path, and deletes it in teardown. Write a test that reads the file and verifies the content.
Solution to Exercise 2
```python import pytest import tempfile import os
@pytest.fixture def temp_file(): fd, path = tempfile.mkstemp() with os.fdopen(fd, 'w') as f: f.write("test content") yield path os.unlink(path)
def test_read_temp_file(temp_file): with open(temp_file) as f: assert f.read() == "test content" ```
Exercise 3. Explain the difference between scope="function" and scope="module" for a pytest fixture. Give an example where module scope would be more appropriate.
Solution to Exercise 3
With scope="function" (the default), the fixture is created and destroyed for every test function. With scope="module", the fixture is created once for the entire module and shared across all tests in that module.
Module scope is appropriate for expensive resources like database connections:
python
@pytest.fixture(scope="module")
def db_connection():
conn = create_connection() # expensive
yield conn
conn.close()
Exercise 4. Write a fixture db_connection that prints "Connecting..." on setup and "Disconnecting..." on teardown. Write two tests that use this fixture and verify the setup/teardown messages appear in the correct order.
Solution to Exercise 4
```python import pytest
@pytest.fixture def db_connection(capsys): print("Connecting...") yield "connection_object" print("Disconnecting...")
def test_query(db_connection): assert db_connection == "connection_object"
def test_insert(db_connection): assert db_connection is not None ```