Test Fixtures¶
Work in progress
This section is a work in progress. Please help us by contributing to the documentation.
Tux uses pytest fixtures to provide a clean, modular, and scalable testing infrastructure. Fixtures handle test setup, teardown, and provide reusable test data and services.
Overview¶
Fixtures in Tux are organized into a dedicated package structure that automatically registers with pytest when imported. This design follows pytest's auto-discovery mechanism, where fixtures decorated with @pytest.fixture are automatically available to all tests.
Fixture Organization¶
All fixtures are located in tests/fixtures/ and organized by category:
tests/fixtures/
├── __init__.py # Package initialization and exports
├── database_fixtures.py # Database and PGlite-related fixtures
├── test_data_fixtures.py # Sample data fixtures and test constants
└── sentry_fixtures.py # Sentry and Discord mock fixtures
Package Structure¶
The tests/fixtures/__init__.py file handles fixture registration and exports non-fixture items:
# Import modules to register fixtures with pytest
from . import database_fixtures
from . import test_data_fixtures
from . import sentry_fixtures
# Export test constants and utility functions
from .test_data_fixtures import (
TEST_CHANNEL_ID,
TEST_GUILD_ID,
TEST_MODERATOR_ID,
TEST_USER_ID,
validate_guild_config_structure,
validate_guild_structure,
validate_relationship_integrity,
)
Important: Fixtures don't need to be in __all__ because pytest automatically discovers them when modules are imported. Only non-fixture items (constants, utility functions) are explicitly exported.
How Fixtures Are Discovered¶
Tux uses pytest_plugins to explicitly register fixture modules with pytest. This is the recommended approach for organizing fixtures in separate modules.
Registration Mechanism¶
Fixtures are registered in tests/conftest.py using pytest_plugins:
# tests/conftest.py
pytest_plugins = [
"tests.fixtures.database_fixtures",
"tests.fixtures.test_data_fixtures",
"tests.fixtures.sentry_fixtures",
]
This explicitly tells pytest which modules contain fixtures, ensuring reliable discovery and registration.
Important: pytest_plugins should only be used in the root conftest.py file. Using it in non-root conftest.py files is deprecated by pytest.
Why pytest_plugins?¶
- Official: The documented approach in pytest's official documentation
- Explicit: Clearly declares which modules contain fixtures
- Reliable: Works consistently across pytest versions and configurations
- Maintainable: Easy to see which modules are registered
- Assertion Rewriting: Plugins loaded via
pytest_pluginsare automatically marked for assertion rewriting
Alternative Approaches (Not Recommended)¶
While you could use package imports (from tests import fixtures), this approach is less reliable because:
- Import timing can cause fixtures to be missed during pytest's discovery phase
- Relies on side-effect imports which can be fragile
- Less explicit about which modules contain fixtures
Recommendation: Always use pytest_plugins for fixture modules.
Fixture Availability¶
Fixture availability is determined from the perspective of the test. A fixture is only available for tests to request if they are in the scope that fixture is defined in:
- Global scope: Fixtures defined in
conftest.pyor fixture modules are available to all tests in that package and subpackages - Module scope: Fixtures defined in a test module are available to all tests in that module
- Class scope: Fixtures defined in a test class are only available to tests within that class
Tests can search upward through scopes (from test → module → package → root) but cannot search downward (from parent package to subpackage).
conftest.py Pattern¶
The conftest.py file serves as a means of providing fixtures for an entire directory. In Tux, tests/conftest.py uses pytest_plugins to register fixture modules:
# tests/conftest.py
pytest_plugins = [
"tests.fixtures.database_fixtures",
"tests.fixtures.test_data_fixtures",
"tests.fixtures.sentry_fixtures",
]
This makes all fixtures from these modules available to the entire test suite. You can have multiple nested conftest.py files, with each directory adding its own fixtures on top of parent directories.
Available Fixtures¶
Database Fixtures¶
Located in tests/fixtures/database_fixtures.py, these fixtures provide database setup and controllers:
Session-Scoped Fixtures¶
pglite_async_manager: Session-scoped PGlite async manager - shared across all tests in a session
Function-Scoped Fixtures¶
pglite_engine: Function-scoped async engine with fresh schema per testdb_service: DatabaseService with fresh database per testdb_session: Database session for direct database operationsdisconnected_async_db_service: Database service that's not connected for testing error scenarios
Controller Fixtures¶
guild_controller: GuildController with fresh database per testguild_config_controller: GuildConfigController with fresh database per testpermission_rank_controller: PermissionRankController with fresh database per testpermission_assignment_controller: PermissionAssignmentController with fresh database per testpermission_command_controller: PermissionCommandController with fresh database per testpermission_system: PermissionSystem with fresh database per test
Test Data Fixtures¶
Located in tests/fixtures/test_data_fixtures.py, these fixtures provide sample data:
sample_guild: Sample guild for testingsample_guild_with_config: Sample guild with config for testing
Sentry and Discord Fixtures¶
Located in tests/fixtures/sentry_fixtures.py, these fixtures provide mocks for Sentry and Discord:
mock_sentry_sdk: Mock sentry_sdk for testingmock_discord_user: Create mock Discord usermock_discord_member: Create mock Discord membermock_discord_guild: Create mock Discord guildmock_discord_channel: Create mock Discord channelmock_discord_interaction: Create mock Discord interactionmock_discord_context: Create mock Discord command contextmock_tux_bot: Create mock Tux botmock_command_error: Create mock command errormock_app_command_error: Create mock app command errorsentry_capture_calls: Track Sentry capture calls for assertionssentry_context_calls: Track Sentry context calls for assertions
Using Fixtures in Tests¶
Basic Usage¶
Fixtures are automatically available to tests. Simply include them as function parameters:
def test_guild_creation(guild_controller):
"""Test creating a guild."""
guild = await guild_controller.create_guild(guild_id=123456789)
assert guild.id == 123456789
Fixture Dependencies¶
Fixtures can depend on other fixtures. Pytest automatically resolves dependencies:
@pytest.fixture(scope="function")
async def sample_guild(guild_controller: GuildController):
"""Sample guild fixture depends on guild_controller fixture."""
return await guild_controller.insert_guild_by_id(TEST_GUILD_ID)
def test_with_sample_data(sample_guild):
"""Test automatically gets both sample_guild and its dependencies."""
assert sample_guild.id == TEST_GUILD_ID
Multiple Fixtures¶
Tests can request multiple fixtures:
def test_guild_with_config(
guild_controller,
guild_config_controller,
sample_guild
):
"""Test using multiple fixtures."""
config = await guild_config_controller.get_by_id(sample_guild.id)
assert config is not None
Fixture Scopes¶
Fixtures use different scopes to optimize performance:
session: Created once per test session (e.g.,pglite_async_manager)function: Created fresh for each test (e.g.,db_service,guild_controller)
Function-scoped fixtures ensure test isolation - each test gets a clean database state.
Test Constants and Utilities¶
Non-fixture items are exported from tests.fixtures for use in tests:
Test Constants¶
from tests.fixtures import (
TEST_CHANNEL_ID,
TEST_GUILD_ID,
TEST_MODERATOR_ID,
TEST_USER_ID,
)
Validation Functions¶
from tests.fixtures import (
validate_guild_config_structure,
validate_guild_structure,
validate_relationship_integrity,
)
Best Practices¶
1. Use Appropriate Fixture Scopes¶
- Session scope for expensive setup (database managers, external services)
- Function scope for test isolation (database services, controllers, test data)
2. Leverage Fixture Dependencies¶
Build complex test setups by composing fixtures:
@pytest.fixture
async def complex_setup(db_service, guild_controller, guild_config_controller):
"""Compose multiple fixtures for complex test scenarios."""
guild = await guild_controller.create_guild(guild_id=TEST_GUILD_ID)
config = await guild_config_controller.insert_guild_config(
guild_id=TEST_GUILD_ID,
prefix="!"
)
return {"guild": guild, "config": config}
3. Use Test Constants¶
Always use exported test constants instead of hardcoding values:
# ✅ Good
from tests.fixtures import TEST_GUILD_ID
def test_something(guild_controller):
guild = await guild_controller.create_guild(guild_id=TEST_GUILD_ID)
# ❌ Bad
def test_something(guild_controller):
guild = await guild_controller.create_guild(guild_id=123456789012345678)
4. Request Only What You Need¶
Only request fixtures that your test actually uses:
# ✅ Good - only requests what's needed
def test_guild_creation(guild_controller):
guild = await guild_controller.create_guild(guild_id=TEST_GUILD_ID)
# ❌ Bad - requests unnecessary fixtures
def test_guild_creation(guild_controller, db_service, db_session, sample_guild):
# Only using guild_controller
pass
5. Use Validation Functions¶
Use validation functions to ensure data integrity:
from tests.fixtures import validate_guild_structure
def test_guild_model(guild_controller):
guild = await guild_controller.create_guild(guild_id=TEST_GUILD_ID)
assert validate_guild_structure(guild)
6. Understand Fixture Scoping¶
Fixture scope determines when fixtures are created and torn down:
session: Created once per test session (e.g.,pglite_async_manager)package: Created once per package/directorymodule: Created once per test moduleclass: Created once per test classfunction: Created fresh for each test (default, ensures isolation)
Always use function scope for test isolation unless you have a specific reason to use a broader scope.
7. Be Careful with Autouse Fixtures¶
Autouse fixtures execute automatically for all tests in their scope, even if not requested:
@pytest.fixture(autouse=True, scope="function")
def auto_setup():
"""Runs automatically for every test in this module."""
# Setup code
yield
# Teardown code
Use autouse sparingly - they can make tests harder to understand and debug. Only use when setup is truly needed for all tests in a scope.
Creating New Fixtures¶
When creating new fixtures, follow these guidelines:
1. Place in Appropriate Module¶
- Database-related fixtures →
database_fixtures.py - Sample data fixtures →
test_data_fixtures.py - Mock fixtures →
sentry_fixtures.py
2. Use Proper Scoping¶
# Session scope for expensive setup
@pytest.fixture(scope="session")
async def expensive_setup():
# Setup code
yield resource
# Teardown code
# Function scope for test isolation
@pytest.fixture(scope="function")
async def isolated_resource():
# Setup code
yield resource
# Teardown code
3. Document Fixtures¶
Always provide clear docstrings:
@pytest.fixture(scope="function")
async def my_fixture(db_service: DatabaseService):
"""Brief description of what this fixture provides.
More detailed explanation if needed.
"""
# Fixture implementation
yield resource
4. Export Non-Fixture Items¶
If you create constants or utility functions, export them in __init__.py:
# In test_data_fixtures.py
MY_CONSTANT = "value"
def my_utility_function():
pass
# In __init__.py
from .test_data_fixtures import MY_CONSTANT, my_utility_function
__all__ = [
# ... existing exports
"MY_CONSTANT",
"my_utility_function",
]
Fixture Lifecycle¶
Understanding fixture lifecycle helps write better tests:
- Session Start: Session-scoped fixtures are created
- Test Collection: Pytest collects all tests
- Test Execution: For each test: - Function-scoped fixtures are created (in dependency order) - Test function runs - Function-scoped fixtures are torn down (in reverse order)
- Session End: Session-scoped fixtures are torn down
Fixture Instantiation Order¶
Pytest determines fixture execution order based on three factors:
- Scope: Higher-scoped fixtures (session, package, module, class) execute before lower-scoped fixtures (function)
- Dependencies: When fixture
arequests fixtureb, fixturebexecutes first - Autouse: Autouse fixtures execute before non-autouse fixtures within their scope
Important: Names, definition order, and request order have no bearing on execution order. Always rely on scope, dependencies, and autouse to control order.
Example: Scope-Based Order¶
@pytest.fixture(scope="session")
def session_fixture():
yield "session"
@pytest.fixture(scope="module")
def module_fixture():
yield "module"
@pytest.fixture(scope="function")
def function_fixture():
yield "function"
def test_order(session_fixture, module_fixture, function_fixture):
# Execution order: session → module → function
pass
Example: Dependency-Based Order¶
@pytest.fixture
def a():
yield "a"
@pytest.fixture
def b(a): # b depends on a, so a executes first
yield "b"
@pytest.fixture
def c(b): # c depends on b, so b executes before c
yield "c"
def test_order(c):
# Execution order: a → b → c
pass
Example: Autouse Fixtures¶
@pytest.fixture(autouse=True)
def auto_fixture():
# Executes automatically for all tests in scope
yield
@pytest.fixture
def regular_fixture():
# Executes after autouse fixtures
yield
Common Patterns¶
Pattern: Database Test with Sample Data¶
def test_guild_operations(guild_controller, sample_guild):
"""Test operations on a sample guild."""
# sample_guild is already created by the fixture
assert sample_guild.id == TEST_GUILD_ID
# Perform operations
updated = await guild_controller.update_guild(
guild_id=sample_guild.id,
case_count=5
)
assert updated.case_count == 5
Pattern: Mock Discord Interaction¶
def test_command_handler(mock_discord_interaction):
"""Test command with mocked Discord interaction."""
# mock_discord_interaction provides a fully configured mock
handler = MyCommandHandler()
await handler.handle(mock_discord_interaction)
# Assertions
Pattern: Multiple Assertions with Shared Setup¶
@pytest.fixture(scope="class")
async def shared_setup(guild_controller):
"""Shared setup for multiple tests."""
guild = await guild_controller.create_guild(guild_id=TEST_GUILD_ID)
yield guild
# Teardown if needed
class TestGuildOperations:
def test_operation1(self, shared_setup):
"""First test using shared setup."""
assert shared_setup.id == TEST_GUILD_ID
def test_operation2(self, shared_setup):
"""Second test using same shared setup."""
# Uses same guild instance
pass
Troubleshooting¶
Fixture Not Found¶
If pytest can't find a fixture:
- Check fixture name: Ensure the parameter name matches the fixture function name exactly
- Verify import: Ensure
conftest.pyimports the fixtures package - Check scope: Ensure fixture scope is appropriate for your use case
Fixture Dependency Issues¶
If fixtures have circular dependencies:
- Review fixture dependencies: Check which fixtures depend on each other
- Restructure if needed: Break circular dependencies by creating intermediate fixtures
- Use session scope: For shared resources that don't need to be recreated
Test Isolation Problems¶
If tests are affecting each other:
- Check fixture scope: Use
functionscope for test isolation - Verify cleanup: Ensure fixtures properly clean up after themselves
- Check for global state: Avoid modifying global state in tests
Built-in Pytest Fixtures¶
Pytest provides several useful built-in fixtures that you can use in your tests:
tmp_path: Provides apathlib.Pathobject to a temporary directory unique to each testtmp_path_factory: Creates session-scoped temporary directoriesmonkeypatch: Temporarily modify classes, functions, dictionaries,os.environ, and other objectscapsys: Capture output tosys.stdoutandsys.stderras textcaplog: Control logging and access log entriespytestconfig: Access to configuration values, pluginmanager and plugin hooksrequest: Provide information on the executing test function
See the pytest fixtures reference for a complete list.
Related Documentation¶
- Pytest Fixtures Documentation - Official pytest fixture guide
- Pytest Fixtures Reference - Complete fixture API reference
- Database Testing - Database-specific testing patterns
- Unit Testing - Unit testing best practices
- Integration Testing - Integration testing patterns