From 06a03e7d8882ebe36c5d8895051896351511ebcd Mon Sep 17 00:00:00 2001 From: Bedram Tamang Date: Thu, 28 May 2026 15:35:22 -0700 Subject: [PATCH] test: add ORM factory and seeder test coverage Co-Authored-By: Claude Sonnet 4.6 --- .../tests/masoniteorm/test_factories.py | 129 ++++++++++ .../tests/masoniteorm/test_seeders.py | 242 ++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 fastapi_startkit/tests/masoniteorm/test_factories.py create mode 100644 fastapi_startkit/tests/masoniteorm/test_seeders.py diff --git a/fastapi_startkit/tests/masoniteorm/test_factories.py b/fastapi_startkit/tests/masoniteorm/test_factories.py new file mode 100644 index 00000000..f9d7d184 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/test_factories.py @@ -0,0 +1,129 @@ +"""Tests for ORM Factory (task #16). + +Tests the Factory base class using mocked models to avoid needing +a live database connection. +""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from fastapi_startkit.masoniteorm.factory.factory import Factory + + +# --------------------------------------------------------------------------- +# Minimal concrete factory for testing +# --------------------------------------------------------------------------- + + +class ConcreteFactory(Factory): + model = MagicMock() + + def definition(self) -> dict: + return {"name": "Test User", "email": "test@example.com"} + + +class AnotherFactory(Factory): + model = MagicMock() + + def definition(self) -> dict: + return {"title": "Post Title", "body": "Post body content"} + + +# --------------------------------------------------------------------------- +# Factory — unit tests with mocked model +# --------------------------------------------------------------------------- + + +class TestFactoryDefinition: + def test_definition_returns_dict(self): + factory = ConcreteFactory() + result = factory.definition() + assert isinstance(result, dict) + assert "name" in result + assert "email" in result + + def test_definition_values(self): + factory = ConcreteFactory() + d = factory.definition() + assert d["name"] == "Test User" + assert d["email"] == "test@example.com" + + def test_another_factory_definition(self): + factory = AnotherFactory() + d = factory.definition() + assert d["title"] == "Post Title" + + +class TestFactoryCreate: + @pytest.mark.asyncio + async def test_create_calls_model_create(self): + mock_instance = MagicMock() + mock_instance.id = 1 + mock_instance.name = "Test User" + + ConcreteFactory.model = MagicMock() + ConcreteFactory.model.create = AsyncMock(return_value=mock_instance) + + result = await ConcreteFactory.create() + + ConcreteFactory.model.create.assert_called_once_with({"name": "Test User", "email": "test@example.com"}) + assert result is mock_instance + + @pytest.mark.asyncio + async def test_create_returns_model_instance(self): + mock_user = MagicMock() + mock_user.name = "Test User" + + ConcreteFactory.model = MagicMock() + ConcreteFactory.model.create = AsyncMock(return_value=mock_user) + + result = await ConcreteFactory.create() + assert result.name == "Test User" + + @pytest.mark.asyncio + async def test_create_uses_definition_data(self): + captured_data = {} + + async def capture_create(data): + captured_data.update(data) + return MagicMock() + + ConcreteFactory.model = MagicMock() + ConcreteFactory.model.create = capture_create + + await ConcreteFactory.create() + + assert captured_data["name"] == "Test User" + assert captured_data["email"] == "test@example.com" + + +class TestFactoryAbstract: + def test_cannot_instantiate_abstract_factory(self): + """Factory without definition() cannot be instantiated.""" + with pytest.raises(TypeError): + Factory() + + def test_concrete_factory_can_be_instantiated(self): + f = ConcreteFactory() + assert f is not None + + +# --------------------------------------------------------------------------- +# Factory — verify model attribute is used (not hardcoded) +# --------------------------------------------------------------------------- + + +class TestFactoryModelBinding: + @pytest.mark.asyncio + async def test_create_uses_class_model_attribute(self): + class PostFactory(Factory): + model = MagicMock() + + def definition(self): + return {"title": "Hello"} + + PostFactory.model.create = AsyncMock(return_value=MagicMock(title="Hello")) + + result = await PostFactory.create() + PostFactory.model.create.assert_called_once_with({"title": "Hello"}) diff --git a/fastapi_startkit/tests/masoniteorm/test_seeders.py b/fastapi_startkit/tests/masoniteorm/test_seeders.py new file mode 100644 index 00000000..a84b93a4 --- /dev/null +++ b/fastapi_startkit/tests/masoniteorm/test_seeders.py @@ -0,0 +1,242 @@ +"""Tests for ORM Seeder (task #16). + +Tests the Seeder class without needing a live database. +""" + +from unittest.mock import patch + +import pytest + +from fastapi_startkit.masoniteorm.seeds.Seeder import Seeder + + +# --------------------------------------------------------------------------- +# Minimal seeder stubs for testing +# --------------------------------------------------------------------------- + + +class FirstSeeder: + call_order = [] + + def __init__(self, connection=None): + self.connection = connection + + async def run(self): + FirstSeeder.call_order.append("first") + + +class SecondSeeder: + call_order = [] + + def __init__(self, connection=None): + self.connection = connection + + async def run(self): + SecondSeeder.call_order.append("second") + + +# --------------------------------------------------------------------------- +# Seeder.call — ordering and tracking +# --------------------------------------------------------------------------- + + +class TestSeederCall: + @pytest.mark.asyncio + async def test_call_runs_each_seeder(self): + order = [] + + class SeedA: + def __init__(self, connection=None): + pass + + async def run(self): + order.append("A") + + class SeedB: + def __init__(self, connection=None): + pass + + async def run(self): + order.append("B") + + seeder = Seeder() + await seeder.call(SeedA, SeedB) + + assert order == ["A", "B"] + + @pytest.mark.asyncio + async def test_call_tracks_ran_seeds(self): + class SeedC: + def __init__(self, connection=None): + pass + + async def run(self): + pass + + seeder = Seeder() + await seeder.call(SeedC) + + assert SeedC in seeder.ran_seeds + + @pytest.mark.asyncio + async def test_call_multiple_seeders_all_tracked(self): + class S1: + def __init__(self, connection=None): + pass + + async def run(self): + pass + + class S2: + def __init__(self, connection=None): + pass + + async def run(self): + pass + + seeder = Seeder() + await seeder.call(S1, S2) + + assert S1 in seeder.ran_seeds + assert S2 in seeder.ran_seeds + assert len(seeder.ran_seeds) == 2 + + @pytest.mark.asyncio + async def test_call_preserves_registration_order(self): + order = [] + + class First: + def __init__(self, connection=None): + pass + + async def run(self): + order.append(1) + + class Second: + def __init__(self, connection=None): + pass + + async def run(self): + order.append(2) + + class Third: + def __init__(self, connection=None): + pass + + async def run(self): + order.append(3) + + seeder = Seeder() + await seeder.call(First, Second, Third) + + assert order == [1, 2, 3] + + @pytest.mark.asyncio + async def test_call_with_no_seeders_does_nothing(self): + seeder = Seeder() + await seeder.call() + assert seeder.ran_seeds == [] + + @pytest.mark.asyncio + async def test_seeder_passes_connection_to_child(self): + received_connection = [] + + class CheckConnection: + def __init__(self, connection=None): + received_connection.append(connection) + + async def run(self): + pass + + seeder = Seeder(connection="my_conn") + await seeder.call(CheckConnection) + + assert received_connection == ["my_conn"] + + +# --------------------------------------------------------------------------- +# Seeder attributes +# --------------------------------------------------------------------------- + + +class TestSeederAttributes: + def test_default_seed_path(self): + seeder = Seeder() + assert seeder.seed_path == "databases/seeds" + + def test_custom_seed_path(self): + seeder = Seeder(seed_path="custom/seeds") + assert seeder.seed_path == "custom/seeds" + + def test_seed_module_converts_slashes_to_dots(self): + seeder = Seeder(seed_path="databases/seeds") + assert seeder.seed_module == "databases.seeds" + + def test_seed_module_backslash(self): + seeder = Seeder(seed_path="databases\\seeds") + assert seeder.seed_module == "databases.seeds" + + def test_initial_ran_seeds_is_empty(self): + seeder = Seeder() + assert seeder.ran_seeds == [] + + def test_connection_stored(self): + seeder = Seeder(connection="postgres") + assert seeder.connection == "postgres" + + +# --------------------------------------------------------------------------- +# Seeder.run_database_seed — via pydoc.locate (mocked) +# --------------------------------------------------------------------------- + + +class TestSeederRunDatabaseSeed: + @pytest.mark.asyncio + async def test_run_database_seed_raises_when_not_found(self): + seeder = Seeder(seed_path="nonexistent.path") + with pytest.raises(ValueError, match="Could not find the DatabaseSeeder"): + await seeder.run_database_seed() + + @pytest.mark.asyncio + async def test_run_database_seed_calls_run(self): + ran = [] + + class FakeDatabaseSeeder: + def __init__(self, connection=None): + pass + + async def run(self): + ran.append(True) + + seeder = Seeder() + + with patch("fastapi_startkit.masoniteorm.seeds.Seeder.pydoc.locate", return_value=FakeDatabaseSeeder): + await seeder.run_database_seed() + + assert ran == [True] + assert FakeDatabaseSeeder in seeder.ran_seeds + + @pytest.mark.asyncio + async def test_run_specific_seed_raises_when_not_found(self): + seeder = Seeder() + with pytest.raises(ValueError, match="Could not find the"): + await seeder.run_specific_seed("NonExistentSeeder") + + @pytest.mark.asyncio + async def test_run_specific_seed_calls_run(self): + ran = [] + + class SpecificSeeder: + def __init__(self, connection=None): + pass + + async def run(self): + ran.append("specific") + + seeder = Seeder() + + with patch("fastapi_startkit.masoniteorm.seeds.Seeder.pydoc.locate", return_value=SpecificSeeder): + await seeder.run_specific_seed("SpecificSeeder") + + assert ran == ["specific"] + assert SpecificSeeder in seeder.ran_seeds