Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attach pytest request fixture to factory class #229

Open
ryancausey opened this issue Dec 11, 2024 · 0 comments
Open

Attach pytest request fixture to factory class #229

ryancausey opened this issue Dec 11, 2024 · 0 comments

Comments

@ryancausey
Copy link

I have an existing fixture that will automatically create a test database and return a client attached to that database for a test, and destroy the test database afterwards. I want to use this fixture to build a Django-like model factory, but I'm running into issues with how to inject this fixture's return value into the Factory.

Here's what I've come up with so far:

# factories.py
class FaunaFactory(factory.Factory):
    """This class overrides the default Factory to save the object to Fauna when the
    strategy is `create`.

    This expects the `Meta.model` of the inheriting class to be set to
    `named_model(dict, "name here")`.
    """

    class Meta:
        abstract = True

    # This is expected to be injected via pytest-factoryboy fixture specialization.
    fauna_test_client = None

    @classmethod
    def _create(cls, model_class, *args, **kwargs):
        """This overrides the normal _create so we can instead save the data to Fauna
        and return the created document.
        """
        data = super()._create(model_class, *args, **kwargs)

        # Remove the fauna test client from the data.
        fauna_test_client = data.pop("fauna_test_client")

        # Use it to store the data in fauna and return the result.
        return fauna_test_client.query(
            fql(
                """
                let collection = Collection(${collection_name})
                collection.create(${kwargs})
                """,
                collection_name=model_class.__name__,
                kwargs=data,
            )
        ).data

    @classmethod
    def _build(cls, model_class, *args, **kwargs):
        """This overrides the normal _build so we can remove the `fauna_test_client`
        key/value pair from the data, as this is only used internally when actually
        creating the object in fauna.
        """
        data = super()._build(model_class, *args, **kwargs)
        del data["fauna_test_client"]
        return data

# conftest.py
for factory_class in (
    AccountFactory,
    CustomerFactory,
    FalseAlarmFeeScheduleItemFactory,
    FalseAlarmSchedulePeriodFactory,
    LateFeeScheduleItemFactory,
    PermitFeeScheduleItemFactory,
):
    register(factory_class, fauna_test_client=LazyFixture("fauna_test_client_with_fsl"))

This is janky, because it only applies the lazy fixture to the model fixture, and not the factory fixture. I attempted to rectify this by overriding the factory fixture like so:

# conftest.py
@pytest.fixture
def fixup_factory_fixture(fauna_test_client_with_fsl):
    """Fixup the provided factory such that it replaces the pytest-factoryboy provided
    factory fixture.

    Since we haven't found a good way to inject the `fauna_test_client` arg without
    having it be an actual argument to the factory, and that the factory fixture doesn't
    benefit from the fixture specialization done in the register call, we need to
    override each factory fixture to inject the `fauna_test_client_with_fsl` into each
    factory and its sub-factories that need the test client.

    We use `functools.partial` to inject the test client but leave the result as a
    callable so to the user it has the same API as the regular factory fixture.
    """

    def _inner(factory_class, fauna_test_client_paths):
        return functools.partial(
            factory_class,
            **{path: fauna_test_client_with_fsl for path in fauna_test_client_paths},
        )

    return _inner

@pytest.fixture
def account_factory(fixup_factory_fixture):
    """Override the default factory fixture with our fixed up one."""
    return fixup_factory_fixture(
        factory_class=AccountFactory, fauna_test_client_paths=("fauna_test_client",)
    )

However, this simply results in the error TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases when calling model_fixture due to it assuming the return value of the factory fixture is always the factory class.

I think ultimately, this would be better solved if there was some way to access the pytest request object within the FactoryClass itself. That way, within the overridden _create classmethod, I could use request.getfixturevalue("fauna_test_client_with_fsl") to retrieve the test client fixture instead of trying to hack it in as a field value.

I'm not sure if this is possible, since I don't know if the factory fixtures and model fixtures are evaluated on every new test function execution, and thus would have the pytest request fixture available. Does anyone know if this is possible?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant