FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.48k stars 392 forks source link

Ability to bypass subfactories and return existing object #560

Open riconnon opened 5 years ago

riconnon commented 5 years ago

Description

I have a scenario where I need to, based on some condition, ensure that a factory returns an existing object (possibly customising it to match the provided fields) rather than creating a new object. I tried overriding _create to do this but it seems like subfactories have already been called before this point.

I'm not sure if there's an existing extension point where I can sensible make this choice or if changes to factory_boy itself would be needed

Model / Factory code
class ModelFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = 'app.Model'

    fk = factory.SubFactory(OtherModel)

    @classmethod
    def _create(cls, *args, **kwargs):
        if condition:
            model = Model.objects.get()
            for k, v in kwargs.items():
                if k in ('field1', 'field2'):
                    setattr(model, k, v)
            model.save()
            return model
        else:
            return super()._create(*args, **kwargs)
The issue

Using the above code the behaviour is fine, except that the SubFactory for OtherModel gets called even in the case of "condition"

Notes

I'd like to know if there's an existing pattern for this case in factory_boy or whether you'd be open to accepting a change to support it.

rbarrois commented 5 years ago

This is indeed a longstanding, complex issue (see #69 !).

There aren't any patterns yet, this would need to change the core builder to perform the following steps:

  1. Gather a list of "get_or_create fields"
  2. Build their values
  3. Run the get_or_create
  4. Branch depending on whether we had to create an instance.

Any help would be very welcome!

intiocean commented 2 years ago

I have managed to do something similar (I'm not modifying the returned value) but fetching an existing object based on a parameter passed to the factory and sharing here in case it is useful to others. Here if default_type is passed into the factory then we fetch an object from the database instead of creating one by calling it's factory)

class ActivityFactory(DjangoModelFactory):
    class Meta:
        model = Activity

    class Params:
        default_type = None

    date = FuzzyDateTime(start_dt=datetime(2010, 1, 1, tzinfo=pytz.utc), end_dt=now(), force_microsecond=0)

    @factory.lazy_attribute
    def type(self):
        if self.default_type:
            return ActivityType.objects.get(default_type=self.default_type)
        return ActivityTypeFactory()