297 lines
6.6 KiB
Markdown
297 lines
6.6 KiB
Markdown
|
|
# Fixtures
|
||
|
|
|
||
|
|
## Table of Contents
|
||
|
|
- [Basic Fixtures](#basic-fixtures)
|
||
|
|
- [Fixture Scopes](#fixture-scopes)
|
||
|
|
- [Fixture Parameters](#fixture-parameters)
|
||
|
|
- [Conftest.py](#conftestpy)
|
||
|
|
- [Built-in Fixtures](#built-in-fixtures)
|
||
|
|
|
||
|
|
## Basic Fixtures
|
||
|
|
|
||
|
|
### Simple Fixture
|
||
|
|
|
||
|
|
```python
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def user_data():
|
||
|
|
"""Return sample user data."""
|
||
|
|
return {
|
||
|
|
"name": "John Doe",
|
||
|
|
"email": "john@example.com",
|
||
|
|
"age": 30,
|
||
|
|
}
|
||
|
|
|
||
|
|
def test_user_name(user_data):
|
||
|
|
assert user_data["name"] == "John Doe"
|
||
|
|
|
||
|
|
def test_user_email(user_data):
|
||
|
|
assert "@" in user_data["email"]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Fixture with Setup and Teardown
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture
|
||
|
|
def database():
|
||
|
|
"""Setup database, yield, then cleanup."""
|
||
|
|
# Setup
|
||
|
|
db = create_database()
|
||
|
|
db.connect()
|
||
|
|
|
||
|
|
yield db # Test runs here
|
||
|
|
|
||
|
|
# Teardown
|
||
|
|
db.clear()
|
||
|
|
db.disconnect()
|
||
|
|
|
||
|
|
def test_insert(database):
|
||
|
|
database.insert({"id": 1, "name": "Test"})
|
||
|
|
assert database.count() == 1
|
||
|
|
```
|
||
|
|
|
||
|
|
### Fixture Returning Factory
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture
|
||
|
|
def make_user():
|
||
|
|
"""Return a factory function for creating users."""
|
||
|
|
created_users = []
|
||
|
|
|
||
|
|
def _make_user(name: str, email: str = None):
|
||
|
|
user = User(name=name, email=email or f"{name}@example.com")
|
||
|
|
created_users.append(user)
|
||
|
|
return user
|
||
|
|
|
||
|
|
yield _make_user
|
||
|
|
|
||
|
|
# Cleanup all created users
|
||
|
|
for user in created_users:
|
||
|
|
user.delete()
|
||
|
|
|
||
|
|
def test_multiple_users(make_user):
|
||
|
|
user1 = make_user("Alice")
|
||
|
|
user2 = make_user("Bob")
|
||
|
|
assert user1.name != user2.name
|
||
|
|
```
|
||
|
|
|
||
|
|
## Fixture Scopes
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Function scope (default) - runs for each test
|
||
|
|
@pytest.fixture(scope="function")
|
||
|
|
def fresh_data():
|
||
|
|
return {"count": 0}
|
||
|
|
|
||
|
|
# Class scope - runs once per test class
|
||
|
|
@pytest.fixture(scope="class")
|
||
|
|
def class_resource():
|
||
|
|
return ExpensiveResource()
|
||
|
|
|
||
|
|
# Module scope - runs once per test module
|
||
|
|
@pytest.fixture(scope="module")
|
||
|
|
def module_connection():
|
||
|
|
conn = create_connection()
|
||
|
|
yield conn
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
# Session scope - runs once per test session
|
||
|
|
@pytest.fixture(scope="session")
|
||
|
|
def session_config():
|
||
|
|
return load_config()
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scope Hierarchy
|
||
|
|
|
||
|
|
```
|
||
|
|
session (once per test run)
|
||
|
|
└── package (once per test package)
|
||
|
|
└── module (once per test file)
|
||
|
|
└── class (once per test class)
|
||
|
|
└── function (once per test function)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Fixture Parameters
|
||
|
|
|
||
|
|
### Parametrized Fixtures
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
|
||
|
|
def database_type(request):
|
||
|
|
"""Run tests with each database type."""
|
||
|
|
return request.param
|
||
|
|
|
||
|
|
def test_connection(database_type):
|
||
|
|
# This test runs 3 times, once for each database
|
||
|
|
db = create_database(database_type)
|
||
|
|
assert db.connect()
|
||
|
|
```
|
||
|
|
|
||
|
|
### Fixture with IDs
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture(params=[
|
||
|
|
pytest.param({"admin": True}, id="admin"),
|
||
|
|
pytest.param({"admin": False}, id="regular"),
|
||
|
|
])
|
||
|
|
def user_config(request):
|
||
|
|
return request.param
|
||
|
|
|
||
|
|
# Test output shows: test_permissions[admin], test_permissions[regular]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Indirect Parametrization
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture
|
||
|
|
def user(request):
|
||
|
|
"""Create user based on parameter."""
|
||
|
|
role = request.param
|
||
|
|
return User(role=role)
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("user", ["admin", "editor", "viewer"], indirect=True)
|
||
|
|
def test_user_access(user):
|
||
|
|
# user fixture receives each role as request.param
|
||
|
|
assert user.role in ["admin", "editor", "viewer"]
|
||
|
|
```
|
||
|
|
|
||
|
|
## Conftest.py
|
||
|
|
|
||
|
|
### tests/conftest.py
|
||
|
|
|
||
|
|
```python
|
||
|
|
"""Shared fixtures for all tests."""
|
||
|
|
import pytest
|
||
|
|
from sqlalchemy import create_engine
|
||
|
|
from sqlalchemy.orm import sessionmaker
|
||
|
|
|
||
|
|
# Session-scoped database engine
|
||
|
|
@pytest.fixture(scope="session")
|
||
|
|
def engine():
|
||
|
|
return create_engine("sqlite:///:memory:")
|
||
|
|
|
||
|
|
# Function-scoped session with transaction rollback
|
||
|
|
@pytest.fixture(scope="function")
|
||
|
|
def db_session(engine):
|
||
|
|
connection = engine.connect()
|
||
|
|
transaction = connection.begin()
|
||
|
|
Session = sessionmaker(bind=connection)
|
||
|
|
session = Session()
|
||
|
|
|
||
|
|
yield session
|
||
|
|
|
||
|
|
session.close()
|
||
|
|
transaction.rollback()
|
||
|
|
connection.close()
|
||
|
|
|
||
|
|
# Shared test data
|
||
|
|
@pytest.fixture
|
||
|
|
def sample_products():
|
||
|
|
return [
|
||
|
|
{"name": "Widget", "price": 9.99},
|
||
|
|
{"name": "Gadget", "price": 19.99},
|
||
|
|
{"name": "Gizmo", "price": 29.99},
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Nested Conftest Files
|
||
|
|
|
||
|
|
```
|
||
|
|
tests/
|
||
|
|
├── conftest.py # Available to all tests
|
||
|
|
├── unit/
|
||
|
|
│ ├── conftest.py # Available to unit tests only
|
||
|
|
│ └── test_models.py
|
||
|
|
└── integration/
|
||
|
|
├── conftest.py # Available to integration tests only
|
||
|
|
└── test_api.py
|
||
|
|
```
|
||
|
|
|
||
|
|
## Built-in Fixtures
|
||
|
|
|
||
|
|
### tmp_path / tmp_path_factory
|
||
|
|
|
||
|
|
```python
|
||
|
|
def test_create_file(tmp_path):
|
||
|
|
"""tmp_path provides unique temporary directory."""
|
||
|
|
file = tmp_path / "test.txt"
|
||
|
|
file.write_text("Hello, World!")
|
||
|
|
assert file.read_text() == "Hello, World!"
|
||
|
|
|
||
|
|
@pytest.fixture(scope="session")
|
||
|
|
def session_temp_dir(tmp_path_factory):
|
||
|
|
"""Create temp dir for entire session."""
|
||
|
|
return tmp_path_factory.mktemp("session_data")
|
||
|
|
```
|
||
|
|
|
||
|
|
### capsys / capfd
|
||
|
|
|
||
|
|
```python
|
||
|
|
def test_print_output(capsys):
|
||
|
|
"""Capture stdout/stderr."""
|
||
|
|
print("Hello")
|
||
|
|
captured = capsys.readouterr()
|
||
|
|
assert captured.out == "Hello\n"
|
||
|
|
|
||
|
|
def test_file_descriptor_output(capfd):
|
||
|
|
"""Capture at file descriptor level."""
|
||
|
|
import os
|
||
|
|
os.system("echo 'Hello'")
|
||
|
|
captured = capfd.readouterr()
|
||
|
|
assert "Hello" in captured.out
|
||
|
|
```
|
||
|
|
|
||
|
|
### monkeypatch
|
||
|
|
|
||
|
|
```python
|
||
|
|
def test_env_variable(monkeypatch):
|
||
|
|
"""Modify environment temporarily."""
|
||
|
|
monkeypatch.setenv("API_KEY", "test-key")
|
||
|
|
assert os.environ["API_KEY"] == "test-key"
|
||
|
|
|
||
|
|
def test_module_attribute(monkeypatch):
|
||
|
|
"""Modify module attribute temporarily."""
|
||
|
|
monkeypatch.setattr("module.CONFIG", {"debug": True})
|
||
|
|
assert module.CONFIG["debug"] is True
|
||
|
|
|
||
|
|
def test_dict_item(monkeypatch):
|
||
|
|
"""Modify dictionary temporarily."""
|
||
|
|
monkeypatch.setitem(app.settings, "DEBUG", True)
|
||
|
|
```
|
||
|
|
|
||
|
|
### request
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture
|
||
|
|
def resource(request):
|
||
|
|
"""Access test context."""
|
||
|
|
print(f"Running: {request.node.name}")
|
||
|
|
print(f"Module: {request.module.__name__}")
|
||
|
|
print(f"Function: {request.function.__name__}")
|
||
|
|
|
||
|
|
# Access fixture parameters
|
||
|
|
if hasattr(request, "param"):
|
||
|
|
return create_resource(request.param)
|
||
|
|
|
||
|
|
return create_default_resource()
|
||
|
|
```
|
||
|
|
|
||
|
|
### Autouse Fixtures
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.fixture(autouse=True)
|
||
|
|
def setup_logging():
|
||
|
|
"""Automatically runs for every test."""
|
||
|
|
logging.basicConfig(level=logging.DEBUG)
|
||
|
|
yield
|
||
|
|
logging.shutdown()
|
||
|
|
|
||
|
|
@pytest.fixture(autouse=True, scope="session")
|
||
|
|
def global_setup():
|
||
|
|
"""Run once at session start."""
|
||
|
|
initialize_system()
|
||
|
|
yield
|
||
|
|
cleanup_system()
|
||
|
|
```
|