factory_boy
factory_boy copied to clipboard
How to call SubFactory and populate many-to-many dependency at the same time
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.
You can use LazyAttribute I think
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).
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.
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