factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

How to call SubFactory and populate many-to-many dependency at the same time

Open fgs-dbudwin opened this issue 4 years ago • 4 comments

class BazFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Baz

    name = factory.Faker("word")


class FooFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Foo

    @factory.post_generation
    def bazs(self, create, extracted, **kwargs):
        if not create:
            return

        if extracted:
            for baz in extracted:
                self.bazs.add(baz)


class BarFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Bar

    foo = factory.SubFactory(FooFactory, bazs=factories.BazFactory.create_batch(3)) # <-- This throws an error due to the bazs named parameter, if I remove it, it will create this model, but bazs will be empty

Given my example code above, when I call BarFactory.create() I want the resulting Bar model to have its foo instance have 3 Baz objects.

I understand that I can achieve a similar outcome by doing the following, but I would prefer my factories to be able to fully create my objects for me, no matter how many nested objects there are:

foo = factories.FooFactory.create(bazs=factories.BazFactory.create_batch(3)))
bar = factories.BarFactory.create(foo=foo)

I assumed I missed something in the docs or their corresponding examples. I just want to be able to call create() on my factory in a test for the specific model I'm concerned about and not having to worry about setting up the inner-models of my model I'm testing.

fgs-dbudwin avatar Jul 08 '20 17:07 fgs-dbudwin

You can use LazyAttribute I think

RedMoon32 avatar Jul 14 '20 09:07 RedMoon32

You can use LazyAttribute I think

I'm not sure how to do this "properly". What seems to work is if I call a Factory inside a LazyAttribute directly, e.g.:

class VulnerabilityFactory(AbstractVulnerabilityFactory):

    pk = factory.LazyFunction(
        lambda: _random_pk(
            v_models.Vulnerability, range(20001, 29999)))

    class Meta:
        model = v_models.Vulnerability


class VulnerabilityWithCPEsFactory(VulnerabilityFactory):

    class Params:
        cpe_list = []
        vuln_source_kwargs = {}

    vulnerability_sources = factory.LazyAttribute(
        lambda o: [
            VulnerabilitySourceFactory(
                source=source,
                vuln_id=o.vuln_id,
                cpes=[{'uri2_3': uri} for uri in o.cpe_list],
                declassify=o.declassify,
                **o.vuln_source_kwargs)
            for source in o.sources])

(This is actual real-life code from application that has been using several layers of legacy fixtures for 7 years, starting from django-nose at some point. I've elided some base classes.)

Which is then called from an old fixture function I'm just porting to FactoryBoy via:

    return VulnerabilityWithCPEsFactory(
        vuln_id=vuln_id,
        cpe_list=cpe_list,
        sources=['vuln_with_cpes'],
        aggregator_class='vuln_with_cpes',
        cvsst_source='vuln_with_cpes',
        cvssb_source='vuln_with_cpes',
        declassify=True,
        vuln_source_kwargs=kwargs,
        **kwargs
    )

First I'm not sure if calling the Factory directly within the LazyAttribute may have any unwanted side effects. Tests which I just refactored to use this Factory are green, though, so it's probably ok.

Second what's not exactly pretty is the "vuln_source_kwargs" parameter, which just passes in kwargs (because some old tests rely on kwargs being passed to the related model).

TauPan avatar Oct 08 '20 13:10 TauPan

Calling the factory in a LazyAttribute doesn't work for me, I'm getting the following error:

TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use positions.set() instead.

voidus avatar Nov 04 '22 18:11 voidus

I couldn't get this to work with LazyAttribute either.

If i'm understanding the desired outcome of the original OP's issue correctly, you could handle this in the @post_generation decorator

class BazFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Baz

    name = factory.Faker("word")


class FooFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Foo

    @factory.post_generation
    def bazs(self, create, extracted, **kwargs):
        if not create:
            return

        self.bazs.set(extracted if extracted else [BazFactory() for _ in range(2)])


class BarFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Bar

if you need to forward args (or dynamically set the number of baz's created) see this SO answer

jamesbrobb avatar Nov 03 '23 10:11 jamesbrobb