Files
fastapi-toolsets/tests/test_fixtures.py

758 lines
22 KiB
Python

"""Tests for fastapi_toolsets.fixtures module."""
import uuid
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi_toolsets.fixtures import (
Context,
FixtureRegistry,
LoadStrategy,
get_obj_by_attr,
load_fixtures,
load_fixtures_by_context,
)
from .conftest import Role, User
class TestContext:
"""Tests for Context enum."""
def test_base_context(self):
"""BASE context has correct value."""
assert Context.BASE.value == "base"
def test_production_context(self):
"""PRODUCTION context has correct value."""
assert Context.PRODUCTION.value == "production"
def test_development_context(self):
"""DEVELOPMENT context has correct value."""
assert Context.DEVELOPMENT.value == "development"
def test_testing_context(self):
"""TESTING context has correct value."""
assert Context.TESTING.value == "testing"
class TestLoadStrategy:
"""Tests for LoadStrategy enum."""
def test_insert_strategy(self):
"""INSERT strategy has correct value."""
assert LoadStrategy.INSERT.value == "insert"
def test_merge_strategy(self):
"""MERGE strategy has correct value."""
assert LoadStrategy.MERGE.value == "merge"
def test_skip_existing_strategy(self):
"""SKIP_EXISTING strategy has correct value."""
assert LoadStrategy.SKIP_EXISTING.value == "skip_existing"
class TestFixtureRegistry:
"""Tests for FixtureRegistry class."""
def test_register_with_decorator(self):
"""Register fixture with decorator."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
@registry.register
def roles():
return [Role(id=role_id, name="admin")]
assert "roles" in [f.name for f in registry.get_all()]
def test_register_with_custom_name(self):
"""Register fixture with custom name."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
@registry.register(name="custom_roles")
def roles():
return [Role(id=role_id, name="admin")]
fixture = registry.get("custom_roles")
assert fixture.name == "custom_roles"
def test_register_with_dependencies(self):
"""Register fixture with dependencies."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
user_id = uuid.uuid4()
@registry.register
def roles():
return [Role(id=role_id, name="admin")]
@registry.register(depends_on=["roles"])
def users():
return [
User(
id=user_id,
username="admin",
email="admin@test.com",
role_id=role_id,
)
]
fixture = registry.get("users")
assert fixture.depends_on == ["roles"]
def test_register_with_contexts(self):
"""Register fixture with contexts."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
@registry.register(contexts=[Context.TESTING])
def test_data():
return [Role(id=role_id, name="test")]
fixture = registry.get("test_data")
assert Context.TESTING.value in fixture.contexts
def test_get_raises_key_error(self):
"""Get raises KeyError for missing fixture."""
registry = FixtureRegistry()
with pytest.raises(KeyError, match="not found"):
registry.get("nonexistent")
def test_get_all(self):
"""Get all registered fixtures."""
registry = FixtureRegistry()
@registry.register
def fixture1():
return []
@registry.register
def fixture2():
return []
fixtures = registry.get_all()
names = {f.name for f in fixtures}
assert names == {"fixture1", "fixture2"}
def test_get_by_context(self):
"""Get fixtures by context."""
registry = FixtureRegistry()
@registry.register(contexts=[Context.BASE])
def base_data():
return []
@registry.register(contexts=[Context.TESTING])
def test_data():
return []
@registry.register(contexts=[Context.PRODUCTION])
def prod_data():
return []
testing_fixtures = registry.get_by_context(Context.TESTING)
names = {f.name for f in testing_fixtures}
assert names == {"test_data"}
class TestIncludeRegistry:
"""Tests for FixtureRegistry.include_registry method."""
def test_include_empty_registry(self):
"""Include an empty registry does nothing."""
main_registry = FixtureRegistry()
other_registry = FixtureRegistry()
@main_registry.register
def roles():
return []
main_registry.include_registry(other_registry)
assert len(main_registry.get_all()) == 1
def test_include_registry_adds_fixtures(self):
"""Include registry adds all fixtures from the other registry."""
main_registry = FixtureRegistry()
other_registry = FixtureRegistry()
@main_registry.register
def roles():
return []
@other_registry.register
def users():
return []
@other_registry.register
def posts():
return []
main_registry.include_registry(other_registry)
names = {f.name for f in main_registry.get_all()}
assert names == {"roles", "users", "posts"}
def test_include_registry_preserves_dependencies(self):
"""Include registry preserves fixture dependencies."""
main_registry = FixtureRegistry()
other_registry = FixtureRegistry()
@main_registry.register
def roles():
return []
@other_registry.register(depends_on=["roles"])
def users():
return []
main_registry.include_registry(other_registry)
fixture = main_registry.get("users")
assert fixture.depends_on == ["roles"]
def test_include_registry_preserves_contexts(self):
"""Include registry preserves fixture contexts."""
main_registry = FixtureRegistry()
other_registry = FixtureRegistry()
@other_registry.register(contexts=[Context.TESTING, Context.DEVELOPMENT])
def test_data():
return []
main_registry.include_registry(other_registry)
fixture = main_registry.get("test_data")
assert Context.TESTING.value in fixture.contexts
assert Context.DEVELOPMENT.value in fixture.contexts
def test_include_registry_raises_on_duplicate(self):
"""Include registry raises ValueError on duplicate fixture names."""
main_registry = FixtureRegistry()
other_registry = FixtureRegistry()
@main_registry.register(name="roles")
def roles_main():
return []
@other_registry.register(name="roles")
def roles_other():
return []
with pytest.raises(ValueError, match="already exists"):
main_registry.include_registry(other_registry)
def test_include_multiple_registries(self):
"""Include multiple registries sequentially."""
main_registry = FixtureRegistry()
dev_registry = FixtureRegistry()
test_registry = FixtureRegistry()
@main_registry.register
def base():
return []
@dev_registry.register
def dev_data():
return []
@test_registry.register
def test_data():
return []
main_registry.include_registry(dev_registry)
main_registry.include_registry(test_registry)
names = {f.name for f in main_registry.get_all()}
assert names == {"base", "dev_data", "test_data"}
class TestDefaultContexts:
"""Tests for FixtureRegistry default contexts."""
def test_default_contexts_applied_to_fixtures(self):
"""Default contexts are applied when no contexts specified."""
registry = FixtureRegistry(contexts=[Context.TESTING])
@registry.register
def test_data():
return []
fixture = registry.get("test_data")
assert fixture.contexts == [Context.TESTING.value]
def test_explicit_contexts_override_default(self):
"""Explicit contexts override default contexts."""
registry = FixtureRegistry(contexts=[Context.TESTING])
@registry.register(contexts=[Context.PRODUCTION])
def prod_data():
return []
fixture = registry.get("prod_data")
assert fixture.contexts == [Context.PRODUCTION.value]
def test_no_default_contexts_uses_base(self):
"""Without default contexts, BASE is used."""
registry = FixtureRegistry()
@registry.register
def data():
return []
fixture = registry.get("data")
assert fixture.contexts == [Context.BASE.value]
def test_multiple_default_contexts(self):
"""Multiple default contexts are applied."""
registry = FixtureRegistry(contexts=[Context.DEVELOPMENT, Context.TESTING])
@registry.register
def dev_test_data():
return []
fixture = registry.get("dev_test_data")
assert Context.DEVELOPMENT.value in fixture.contexts
assert Context.TESTING.value in fixture.contexts
def test_default_contexts_with_string_values(self):
"""Default contexts work with string values."""
registry = FixtureRegistry(contexts=["custom_context"])
@registry.register
def custom_data():
return []
fixture = registry.get("custom_data")
assert fixture.contexts == ["custom_context"]
class TestDependencyResolution:
"""Tests for fixture dependency resolution."""
def test_resolve_simple_dependency(self):
"""Resolve simple dependency chain."""
registry = FixtureRegistry()
@registry.register
def roles():
return []
@registry.register(depends_on=["roles"])
def users():
return []
order = registry.resolve_dependencies("users")
assert order == ["roles", "users"]
def test_resolve_multiple_dependencies(self):
"""Resolve multiple dependencies."""
registry = FixtureRegistry()
@registry.register
def roles():
return []
@registry.register
def permissions():
return []
@registry.register(depends_on=["roles", "permissions"])
def users():
return []
order = registry.resolve_dependencies("users")
assert "roles" in order
assert "permissions" in order
assert order.index("roles") < order.index("users")
assert order.index("permissions") < order.index("users")
def test_resolve_transitive_dependencies(self):
"""Resolve transitive dependencies."""
registry = FixtureRegistry()
@registry.register
def base():
return []
@registry.register(depends_on=["base"])
def middle():
return []
@registry.register(depends_on=["middle"])
def top():
return []
order = registry.resolve_dependencies("top")
assert order == ["base", "middle", "top"]
def test_detect_circular_dependency(self):
"""Detect circular dependencies."""
registry = FixtureRegistry()
@registry.register(depends_on=["b"])
def a():
return []
@registry.register(depends_on=["a"])
def b():
return []
with pytest.raises(ValueError, match="Circular dependency"):
registry.resolve_dependencies("a")
def test_resolve_context_dependencies(self):
"""Resolve all fixtures for a context with dependencies."""
registry = FixtureRegistry()
@registry.register(contexts=[Context.BASE])
def roles():
return []
@registry.register(depends_on=["roles"], contexts=[Context.TESTING])
def test_users():
return []
order = registry.resolve_context_dependencies(Context.BASE, Context.TESTING)
assert "roles" in order
assert "test_users" in order
assert order.index("roles") < order.index("test_users")
class TestLoadFixtures:
"""Tests for load_fixtures function."""
@pytest.mark.anyio
async def test_load_single_fixture(self, db_session: AsyncSession):
"""Load a single fixture."""
registry = FixtureRegistry()
role_id_1 = uuid.uuid4()
role_id_2 = uuid.uuid4()
@registry.register
def roles():
return [
Role(id=role_id_1, name="admin"),
Role(id=role_id_2, name="user"),
]
result = await load_fixtures(db_session, registry, "roles")
assert "roles" in result
assert len(result["roles"]) == 2
from .conftest import RoleCrud
count = await RoleCrud.count(db_session)
assert count == 2
@pytest.mark.anyio
async def test_load_with_dependencies(self, db_session: AsyncSession):
"""Load fixtures with dependencies."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
user_id = uuid.uuid4()
@registry.register
def roles():
return [Role(id=role_id, name="admin")]
@registry.register(depends_on=["roles"])
def users():
return [
User(
id=user_id,
username="admin",
email="admin@test.com",
role_id=role_id,
)
]
result = await load_fixtures(db_session, registry, "users")
assert "roles" in result
assert "users" in result
from .conftest import RoleCrud, UserCrud
assert await RoleCrud.count(db_session) == 1
assert await UserCrud.count(db_session) == 1
@pytest.mark.anyio
async def test_load_with_merge_strategy(self, db_session: AsyncSession):
"""Load fixtures with MERGE strategy updates existing."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
@registry.register
def roles():
return [Role(id=role_id, name="admin")]
await load_fixtures(db_session, registry, "roles", strategy=LoadStrategy.MERGE)
await load_fixtures(db_session, registry, "roles", strategy=LoadStrategy.MERGE)
from .conftest import RoleCrud
count = await RoleCrud.count(db_session)
assert count == 1
@pytest.mark.anyio
async def test_load_with_skip_existing_strategy(self, db_session: AsyncSession):
"""Load fixtures with SKIP_EXISTING strategy."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
@registry.register
def roles():
return [Role(id=role_id, name="original")]
await load_fixtures(
db_session, registry, "roles", strategy=LoadStrategy.SKIP_EXISTING
)
@registry.register(name="roles_updated")
def roles_v2():
return [Role(id=role_id, name="updated")]
registry._fixtures["roles"] = registry._fixtures.pop("roles_updated")
await load_fixtures(
db_session, registry, "roles", strategy=LoadStrategy.SKIP_EXISTING
)
from .conftest import RoleCrud
role = await RoleCrud.first(db_session, [Role.id == role_id])
assert role is not None
assert role.name == "original"
@pytest.mark.anyio
async def test_load_with_insert_strategy(self, db_session: AsyncSession):
"""Load fixtures with INSERT strategy."""
registry = FixtureRegistry()
role_id_1 = uuid.uuid4()
role_id_2 = uuid.uuid4()
@registry.register
def roles():
return [
Role(id=role_id_1, name="admin"),
Role(id=role_id_2, name="user"),
]
result = await load_fixtures(
db_session, registry, "roles", strategy=LoadStrategy.INSERT
)
assert "roles" in result
assert len(result["roles"]) == 2
from .conftest import RoleCrud
count = await RoleCrud.count(db_session)
assert count == 2
@pytest.mark.anyio
async def test_load_empty_fixture(self, db_session: AsyncSession):
"""Load a fixture that returns an empty list."""
registry = FixtureRegistry()
@registry.register
def empty_roles():
return []
result = await load_fixtures(db_session, registry, "empty_roles")
assert "empty_roles" in result
assert result["empty_roles"] == []
@pytest.mark.anyio
async def test_load_multiple_fixtures_without_dependencies(
self, db_session: AsyncSession
):
"""Load multiple independent fixtures."""
registry = FixtureRegistry()
role_id_1 = uuid.uuid4()
role_id_2 = uuid.uuid4()
@registry.register
def roles():
return [Role(id=role_id_1, name="admin")]
@registry.register
def other_roles():
return [Role(id=role_id_2, name="user")]
result = await load_fixtures(db_session, registry, "roles", "other_roles")
assert "roles" in result
assert "other_roles" in result
from .conftest import RoleCrud
count = await RoleCrud.count(db_session)
assert count == 2
class TestLoadFixturesByContext:
"""Tests for load_fixtures_by_context function."""
@pytest.mark.anyio
async def test_load_by_single_context(self, db_session: AsyncSession):
"""Load fixtures by single context."""
registry = FixtureRegistry()
base_role_id = uuid.uuid4()
test_role_id = uuid.uuid4()
@registry.register(contexts=[Context.BASE])
def base_roles():
return [Role(id=base_role_id, name="base_role")]
@registry.register(contexts=[Context.TESTING])
def test_roles():
return [Role(id=test_role_id, name="test_role")]
await load_fixtures_by_context(db_session, registry, Context.BASE)
from .conftest import RoleCrud
count = await RoleCrud.count(db_session)
assert count == 1
role = await RoleCrud.first(db_session, [Role.id == base_role_id])
assert role is not None
assert role.name == "base_role"
@pytest.mark.anyio
async def test_load_by_multiple_contexts(self, db_session: AsyncSession):
"""Load fixtures by multiple contexts."""
registry = FixtureRegistry()
base_role_id = uuid.uuid4()
test_role_id = uuid.uuid4()
@registry.register(contexts=[Context.BASE])
def base_roles():
return [Role(id=base_role_id, name="base_role")]
@registry.register(contexts=[Context.TESTING])
def test_roles():
return [Role(id=test_role_id, name="test_role")]
await load_fixtures_by_context(
db_session, registry, Context.BASE, Context.TESTING
)
from .conftest import RoleCrud
count = await RoleCrud.count(db_session)
assert count == 2
@pytest.mark.anyio
async def test_load_context_with_dependencies(self, db_session: AsyncSession):
"""Load context fixtures with cross-context dependencies."""
registry = FixtureRegistry()
role_id = uuid.uuid4()
user_id = uuid.uuid4()
@registry.register(contexts=[Context.BASE])
def roles():
return [Role(id=role_id, name="admin")]
@registry.register(depends_on=["roles"], contexts=[Context.TESTING])
def test_users():
return [
User(
id=user_id,
username="tester",
email="test@test.com",
role_id=role_id,
)
]
await load_fixtures_by_context(db_session, registry, Context.TESTING)
from .conftest import RoleCrud, UserCrud
assert await RoleCrud.count(db_session) == 1
assert await UserCrud.count(db_session) == 1
class TestGetObjByAttr:
"""Tests for get_obj_by_attr helper function."""
def setup_method(self):
"""Set up test fixtures for each test."""
self.registry = FixtureRegistry()
self.role_id_1 = uuid.uuid4()
self.role_id_2 = uuid.uuid4()
self.role_id_3 = uuid.uuid4()
self.user_id_1 = uuid.uuid4()
self.user_id_2 = uuid.uuid4()
role_id_1 = self.role_id_1
role_id_2 = self.role_id_2
role_id_3 = self.role_id_3
user_id_1 = self.user_id_1
user_id_2 = self.user_id_2
@self.registry.register
def roles() -> list[Role]:
return [
Role(id=role_id_1, name="admin"),
Role(id=role_id_2, name="user"),
Role(id=role_id_3, name="moderator"),
]
@self.registry.register(depends_on=["roles"])
def users() -> list[User]:
return [
User(
id=user_id_1,
username="alice",
email="alice@example.com",
role_id=role_id_1,
),
User(
id=user_id_2,
username="bob",
email="bob@example.com",
role_id=role_id_1,
),
]
self.roles = roles
self.users = users
def test_get_by_id(self):
"""Get an object by its id attribute."""
role = get_obj_by_attr(self.roles, "id", self.role_id_1)
assert role.name == "admin"
def test_get_user_by_username(self):
"""Get a user by username."""
user = get_obj_by_attr(self.users, "username", "bob")
assert user.id == self.user_id_2
assert user.email == "bob@example.com"
def test_returns_first_match(self):
"""Returns the first matching object when multiple could match."""
user = get_obj_by_attr(self.users, "role_id", self.role_id_1)
assert user.username == "alice"
def test_no_match_raises_stop_iteration(self):
"""Raises StopIteration with contextual message when no object matches."""
with pytest.raises(
StopIteration,
match="No object with name=nonexistent found in fixture 'roles'",
):
get_obj_by_attr(self.roles, "name", "nonexistent")
def test_no_match_on_wrong_value_type(self):
"""Raises StopIteration when value type doesn't match."""
with pytest.raises(StopIteration):
get_obj_by_attr(self.roles, "id", "not-a-uuid")