Skip to content

Attach pytest request fixture to factory class #229

@ryancausey

Description

@ryancausey

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions