Skip to content

Test Fixtures

Reusable test setup components.

Built-in Fixtures

Fastpy provides common fixtures in tests/conftest.py.

client

Async HTTP client for testing endpoints.

python
@pytest.mark.asyncio
async def test_endpoint(client):
    response = await client.get("/api/users")
    assert response.status_code == 200

session

Database session for direct database operations.

python
@pytest.mark.asyncio
async def test_database(session):
    user = User(email="test@example.com", name="Test")
    session.add(user)
    await session.commit()

    result = await session.execute(select(User))
    users = result.scalars().all()
    assert len(users) == 1

test_user

Pre-created user for tests.

python
@pytest.fixture
async def test_user(session):
    user = User(
        email="testuser@example.com",
        password=get_password_hash("password123"),
        name="Test User",
        is_active=True
    )
    session.add(user)
    await session.commit()
    await session.refresh(user)
    return user

Usage:

python
@pytest.mark.asyncio
async def test_with_user(client, test_user):
    # test_user is already in database
    assert test_user.email == "testuser@example.com"

auth_headers

Authorization headers with valid token.

python
@pytest.fixture
async def auth_headers(test_user):
    token = create_access_token(data={"sub": str(test_user.id)})
    return {"Authorization": f"Bearer {token}"}

Usage:

python
@pytest.mark.asyncio
async def test_protected_route(client, auth_headers):
    response = await client.get(
        "/api/users/me",
        headers=auth_headers
    )
    assert response.status_code == 200

Creating Custom Fixtures

Model Fixtures

python
# tests/conftest.py
@pytest.fixture
async def test_post(session, test_user):
    post = Post(
        title="Test Post",
        body="Test content",
        user_id=test_user.id,
        published=True
    )
    session.add(post)
    await session.commit()
    await session.refresh(post)
    return post

Multiple Records

python
@pytest.fixture
async def test_posts(session, test_user):
    posts = []
    for i in range(5):
        post = Post(
            title=f"Post {i+1}",
            body=f"Content {i+1}",
            user_id=test_user.id
        )
        session.add(post)
        posts.append(post)
    await session.commit()
    return posts

Fixture with Parameters

python
@pytest.fixture
def create_user(session):
    async def _create_user(email: str, name: str = "Test"):
        user = User(
            email=email,
            password=get_password_hash("password123"),
            name=name
        )
        session.add(user)
        await session.commit()
        await session.refresh(user)
        return user
    return _create_user

# Usage
@pytest.mark.asyncio
async def test_multiple_users(create_user):
    user1 = await create_user("user1@example.com")
    user2 = await create_user("user2@example.com")
    assert user1.id != user2.id

Fixture Scopes

Function Scope (Default)

Created for each test function.

python
@pytest.fixture
async def test_user(session):
    # Created fresh for each test
    pass

Module Scope

Created once per test module.

python
@pytest.fixture(scope="module")
async def shared_data():
    # Shared across all tests in module
    pass

Session Scope

Created once per test session.

python
@pytest.fixture(scope="session")
def event_loop():
    # One event loop for all tests
    pass

Fixture Dependencies

Fixtures can depend on other fixtures:

python
@pytest.fixture
async def test_user(session):
    # Depends on session fixture
    pass

@pytest.fixture
async def test_post(session, test_user):
    # Depends on session AND test_user
    pass

@pytest.fixture
async def auth_headers(test_user):
    # Depends on test_user
    pass

Auto-use Fixtures

Run automatically for every test:

python
@pytest.fixture(autouse=True)
async def setup_database():
    """Reset database before each test."""
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    yield
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.drop_all)

Fixture Cleanup

Use yield for setup/teardown:

python
@pytest.fixture
async def temp_file():
    # Setup
    path = Path("/tmp/test_file.txt")
    path.write_text("test content")

    yield path

    # Teardown
    path.unlink()

Best Practices

  1. Keep fixtures small - One responsibility each
  2. Use appropriate scope - Balance isolation vs speed
  3. Name clearly - test_user, admin_user, inactive_user
  4. Document dependencies - Clear fixture chains
  5. Clean up resources - Use yield for teardown

Released under the MIT License.