Chapter 3: Fixture Mechanism in Detail
Haiyue
16min
Chapter 3: Fixture Mechanism in Detail
Learning Objectives
- Understand the concept and purpose of fixtures
- Master the definition and use of fixtures
- Learn fixture scope management
- Master the use of built-in fixtures
Knowledge Points
Fixture Concept
Fixtures are one of pytest’s core features, providing an elegant way to:
- Data preparation: Provide preset data for tests
- Resource management: Manage resources required for tests (database connections, temporary files, etc.)
- Environment setup: Set up test environments and cleanup work
- Dependency injection: Inject dependencies into test functions
Fixture Scopes
| Scope | Description | Lifecycle |
|---|---|---|
function | Function level (default) | Executed once per test function |
class | Class level | Executed once per test class |
module | Module level | Executed once per test module |
package | Package level | Executed once per test package |
session | Session level | Executed once for entire test session |
Fixture Decorator Parameters
@pytest.fixture(
scope="function", # Scope
autouse=False, # Whether to use automatically
name=None, # Custom name
params=None, # Parameterization
ids=None # Parameter identifiers
)
Example Code
Basic Fixture Usage
# test_fixture_basic.py
import pytest
@pytest.fixture
def sample_data():
"""Fixture providing test data"""
return {
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
@pytest.fixture
def sample_list():
"""Fixture providing list data"""
return [1, 2, 3, 4, 5]
def test_user_data(sample_data):
"""Test using sample_data fixture"""
assert sample_data["name"] == "Alice"
assert sample_data["age"] == 30
assert "email" in sample_data
def test_list_operations(sample_list):
"""Test using sample_list fixture"""
assert len(sample_list) == 5
assert sum(sample_list) == 15
assert max(sample_list) == 5
def test_multiple_fixtures(sample_data, sample_list):
"""Using multiple fixtures simultaneously"""
assert len(sample_data) == 3
assert len(sample_list) == 5
Fixture Scope Examples
# test_fixture_scope.py
import pytest
# Global counter for demonstration
execution_count = {"function": 0, "class": 0, "module": 0, "session": 0}
@pytest.fixture(scope="function")
def function_fixture():
"""Function-level fixture"""
execution_count["function"] += 1
print(f"\nFunction fixture executed: {execution_count['function']}")
return f"function_data_{execution_count['function']}"
@pytest.fixture(scope="class")
def class_fixture():
"""Class-level fixture"""
execution_count["class"] += 1
print(f"\nClass fixture executed: {execution_count['class']}")
return f"class_data_{execution_count['class']}"
@pytest.fixture(scope="module")
def module_fixture():
"""Module-level fixture"""
execution_count["module"] += 1
print(f"\nModule fixture executed: {execution_count['module']}")
return f"module_data_{execution_count['module']}"
@pytest.fixture(scope="session")
def session_fixture():
"""Session-level fixture"""
execution_count["session"] += 1
print(f"\nSession fixture executed: {execution_count['session']}")
return f"session_data_{execution_count['session']}"
class TestScopeDemo:
def test_first(self, function_fixture, class_fixture, module_fixture, session_fixture):
"""First test"""
assert "function_data" in function_fixture
assert "class_data" in class_fixture
assert "module_data" in module_fixture
assert "session_data" in session_fixture
def test_second(self, function_fixture, class_fixture, module_fixture, session_fixture):
"""Second test"""
# function_fixture will be recreated
# class_fixture will be reused
assert "function_data" in function_fixture
assert "class_data" in class_fixture
def test_outside_class(function_fixture, module_fixture, session_fixture):
"""Test outside the class"""
assert "function_data" in function_fixture
assert "module_data" in module_fixture
assert "session_data" in session_fixture
Database Connection Fixture Example
# test_database_fixture.py
import pytest
import sqlite3
import tempfile
import os
@pytest.fixture(scope="module")
def database_connection():
"""Database connection fixture"""
# Create temporary database file
db_fd, db_path = tempfile.mkstemp(suffix=".db")
# Create connection
conn = sqlite3.connect(db_path)
# Set up database schema
conn.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
)
''')
# Insert test data
conn.execute("INSERT INTO users (name, email) VALUES (?, ?)",
("Alice", "alice@example.com"))
conn.execute("INSERT INTO users (name, email) VALUES (?, ?)",
("Bob", "bob@example.com"))
conn.commit()
yield conn # Provide connection to tests
# Cleanup: close connection and delete file
conn.close()
os.close(db_fd)
os.unlink(db_path)
def test_user_count(database_connection):
"""Test user count"""
cursor = database_connection.execute("SELECT COUNT(*) FROM users")
count = cursor.fetchone()[0]
assert count == 2
def test_user_names(database_connection):
"""Test user names"""
cursor = database_connection.execute("SELECT name FROM users ORDER BY name")
names = [row[0] for row in cursor.fetchall()]
assert names == ["Alice", "Bob"]
def test_insert_user(database_connection):
"""Test inserting a new user"""
database_connection.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Charlie", "charlie@example.com")
)
database_connection.commit()
cursor = database_connection.execute("SELECT COUNT(*) FROM users")
count = cursor.fetchone()[0]
assert count == 3
Filesystem Fixture
# test_filesystem_fixture.py
import pytest
import tempfile
import os
from pathlib import Path
@pytest.fixture
def temp_dir():
"""Temporary directory fixture"""
with tempfile.TemporaryDirectory() as tmp_dir:
yield Path(tmp_dir)
@pytest.fixture
def sample_file(temp_dir):
"""Sample file fixture"""
file_path = temp_dir / "sample.txt"
content = "Hello, World!\nThis is a test file.\n"
with open(file_path, "w") as f:
f.write(content)
return file_path
def test_file_exists(sample_file):
"""Test if file exists"""
assert sample_file.exists()
assert sample_file.is_file()
def test_file_content(sample_file):
"""Test file content"""
content = sample_file.read_text()
assert "Hello, World!" in content
assert content.count("\n") == 2
def test_file_operations(temp_dir):
"""Test file operations"""
# Create file
test_file = temp_dir / "test.txt"
test_file.write_text("Test content")
# Verify file
assert test_file.exists()
assert test_file.read_text() == "Test content"
# Create subdirectory
sub_dir = temp_dir / "subdir"
sub_dir.mkdir()
assert sub_dir.is_dir()
Fixture Dependencies and Composition
# test_fixture_dependency.py
import pytest
@pytest.fixture
def base_config():
"""Base configuration fixture"""
return {
"debug": True,
"host": "localhost"
}
@pytest.fixture
def database_config(base_config):
"""Database configuration fixture (depends on base_config)"""
config = base_config.copy()
config.update({
"database": {
"host": "db.localhost",
"port": 5432,
"name": "testdb"
}
})
return config
@pytest.fixture
def api_client(database_config):
"""API client fixture (depends on database_config)"""
class MockAPIClient:
def __init__(self, config):
self.config = config
self.connected = True
def get_status(self):
return "connected" if self.connected else "disconnected"
def get_database_info(self):
return self.config["database"]
return MockAPIClient(database_config)
def test_config_chain(base_config, database_config, api_client):
"""Test fixture dependency chain"""
# Base configuration
assert base_config["debug"] is True
# Database configuration inherits base configuration
assert database_config["debug"] is True
assert "database" in database_config
# API client uses database configuration
assert api_client.get_status() == "connected"
assert api_client.get_database_info()["host"] == "db.localhost"
Parameterized Fixtures
# test_parametrized_fixture.py
import pytest
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
def database_type(request):
"""Parameterized database type fixture"""
return request.param
@pytest.fixture(params=[
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
])
def user_data(request):
"""Parameterized user data fixture"""
return request.param
def test_database_connection(database_type):
"""Test connection for different database types"""
# This test will run once for each database type
assert database_type in ["sqlite", "postgresql", "mysql"]
print(f"Testing with {database_type} database")
def test_user_validation(user_data):
"""Test validation with different user data"""
# This test will run once for each user data set
assert "name" in user_data
assert "age" in user_data
assert isinstance(user_data["age"], int)
assert user_data["age"] > 0
Auto-use Fixtures
# test_autouse_fixture.py
import pytest
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@pytest.fixture(autouse=True)
def log_test_info(request):
"""Auto-use logging fixture"""
test_name = request.node.name
logger.info(f"Starting test: {test_name}")
yield # Test execution
logger.info(f"Finished test: {test_name}")
@pytest.fixture(autouse=True, scope="class")
def setup_test_class(request):
"""Auto-use class-level fixture"""
if request.cls:
logger.info(f"Setting up test class: {request.cls.__name__}")
yield
logger.info(f"Tearing down test class: {request.cls.__name__}")
else:
yield
def test_simple_operation():
"""Simple test"""
result = 2 + 2
assert result == 4
class TestCalculations:
def test_addition(self):
"""Test addition"""
assert 1 + 1 == 2
def test_multiplication(self):
"""Test multiplication"""
assert 3 * 4 == 12
Built-in Fixtures
# test_builtin_fixtures.py
import pytest
def test_tmp_path_fixture(tmp_path):
"""Test tmp_path built-in fixture"""
# tmp_path provides temporary directory path
assert tmp_path.is_dir()
# Create temporary file
temp_file = tmp_path / "test.txt"
temp_file.write_text("Hello, temporary file!")
assert temp_file.exists()
assert temp_file.read_text() == "Hello, temporary file!"
def test_tmpdir_fixture(tmpdir):
"""Test tmpdir built-in fixture (legacy version)"""
# tmpdir also provides temporary directory, but returns py.path.local object
temp_file = tmpdir.join("test.txt")
temp_file.write("Hello from tmpdir!")
assert temp_file.exists()
assert temp_file.read() == "Hello from tmpdir!"
def test_capsys_fixture(capsys):
"""Test capsys built-in fixture"""
# capsys captures standard output and error
print("Hello, stdout!")
print("Hello, stderr!", file=__import__('sys').stderr)
captured = capsys.readouterr()
assert "Hello, stdout!" in captured.out
assert "Hello, stderr!" in captured.err
def test_monkeypatch_fixture(monkeypatch):
"""Test monkeypatch built-in fixture"""
import os
# Temporarily set environment variable
monkeypatch.setenv("TEST_VAR", "test_value")
assert os.getenv("TEST_VAR") == "test_value"
# Temporarily modify attribute
original_value = getattr(os, 'name', None)
monkeypatch.setattr(os, 'name', 'test_os')
assert os.name == 'test_os'
# Fixture will automatically restore after ending
def test_request_fixture(request):
"""Test request built-in fixture"""
# request provides information about test request
assert request.node.name == "test_request_fixture"
assert hasattr(request, 'config')
assert hasattr(request, 'module')
Fixture Best Practices
conftest.py File
# conftest.py - Project root or test directory
import pytest
@pytest.fixture(scope="session")
def app_config():
"""Application configuration - session level"""
return {
"api_url": "https://api.example.com",
"timeout": 30,
"retries": 3
}
@pytest.fixture
def mock_user():
"""Mock user data"""
return {
"id": 1,
"username": "testuser",
"email": "test@example.com",
"roles": ["user"]
}
@pytest.fixture
def admin_user():
"""Administrator user"""
return {
"id": 2,
"username": "admin",
"email": "admin@example.com",
"roles": ["admin", "user"]
}
Fixture Organization Structure
🔄 正在渲染 Mermaid 图表...
Fixture Usage Tips
- Choose scope wisely: Select appropriate scope based on resource lifecycle
- Use yield for cleanup: Write cleanup code after yield
- Avoid over-complexity: Fixtures should be simple and focused on a single responsibility
- Use conftest.py: Place shared fixtures in conftest.py
- Document fixtures: Write clear docstrings for complex fixtures
Common Pitfalls
- Scope confusion: Be aware of different scope lifecycles
- Circular dependencies: Avoid circular dependencies between fixtures
- State pollution: Ensure fixtures don’t affect other tests
- Resource leaks: Use yield to ensure proper resource cleanup
Fixture vs Setup/Teardown Comparison
| Feature | Fixture | Setup/Teardown |
|---|---|---|
| Code reuse | Excellent | Average |
| Dependency injection | Supported | Not supported |
| Scope control | Flexible | Limited |
| Parameterization | Supported | Not supported |
| Error handling | Excellent | Average |
| Readability | High | Medium |
The fixture mechanism is one of pytest’s highlights. Mastering fixtures means mastering the essence of pytest testing, enabling you to write more flexible and maintainable test code.