Reputation: 57771
I'd like to defined two models, Company
and Package
. Each Package
has only one Company
, but a Company
can have several Packages
. However, each company can have only one default_package
(which can be null). I've set this up as follows:
class Company(models.Model):
default_package = models.OneToOneField(
'dashboard.Package',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='default_for_%(class)s')
class Package(models.Model):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
where dashboard
is the app label.
In order to simplify tests with these models, I've created test factories for them using factory_boy
as follows:
import factory
from .models import Company, Package
class CompanyFactory(factory.Factory):
class Meta:
model = Company
default_package = factory.SubFactory('dashboard.test_factories.PackageFactory')
class PackageFactory(factory.Factory):
class Meta:
model = Package
company = factory.SubFactory(CompanyFactory)
Now I'm trying two tests:
class DefaultPackageTest(TestCase):
def test_1(self):
company = Company.objects.create()
def test_2(self):
company = CompanyFactory()
The first one simply creates a Company
, whereas the second one tries to do the same thing using the CompanyFactory
.
Strangely, however, the first test passes, whereas the second one fails:
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 233, in recurse
return builder.build(parent_step=self, force_sequence=force_sequence)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 272, in build
step.resolve(pre)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 221, in resolve
self.attributes[field_name] = getattr(self.stub, field_name)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 355, in __getattr__
declaration = self.__declarations[name]
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 121, in __getitem__
context=self.contexts[key],
RecursionError: maximum recursion depth exceeded
----------------------------------------------------------------------
Ran 2 tests in 0.012s
FAILED (errors=1)
Destroying test database for alias 'default'...
Any idea why this is not working? I believe I have followed the docs (http://factoryboy.readthedocs.io/en/latest/reference.html#circular-imports) by providing a full path to the CompanyFactory
's sub-factory.
Update
It seems that this use case is addressed by Factory Boy's post-generation hooks. What seems promising is the RelatedFactory
, for which the following example is given:
class CityFactory(factory.Factory):
class Meta:
model = City
capital_of = None
name = "Toronto"
class CountryFactory(factory.Factory):
class Meta:
model = Country
lang = 'fr'
capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris")
which is tested in the Python REPL as follows:
>>> france = CountryFactory()
>>> City.objects.get(capital_of=france)
<City: Paris>
I'm having difficulty, however, applying this example to my situation. (It doesn't help that there is no text explanation or code of the City
and Country
models in the docs). It seems like capital_city
is analogous to default_package
in my case, so I tried turning it into a RelatedFactory
like so,
default_package = factory.RelatedFactory('dashboard.test_factories.PackageFactory')
but I still get the same error.
Upvotes: 5
Views: 1424
Reputation: 339
Quoting from the specific documentation of RelatedFactory, you require a RelatedFactory
as the Package
model requires a company
to be built first:
A RelatedFactory behaves mostly like a SubFactory, with the main difference that the related Factory will be generated after the base Factory.
and will need to precise the following parameter:
factory_related_name
If set, the object generated by the factory declaring the RelatedFactory is passed as keyword argument to the related factory.
In your case, if we put those two together:
class PackageFactory(factory.Factory):
class Meta:
model = Package
company = factory.SubFactory('dashboard.test_factories.CompanyFactory')
class CompanyFactory(factory.Factory):
class Meta:
model = Company
# Putting the pieces together
default_package = factory.RelatedFactory(
PackageFactory,
factory_related_name='company'
)
It is equivalent to Gustavo's solution with the @factory.post_generation
decorator, but follows Factory Boy's declarative mindset more closely in my opinion.
Upvotes: 0
Reputation: 111
So, I achieved that by implementing a post_generation
function:
class PackageFactory(factory.Factory):
class Meta:
model = Package
company = factory.SubFactory('dashboard.test_factories.CompanyFactory')
class CompanyFactory(factory.Factory):
class Meta:
model = Company
@factory.post_generation
def default_package(self, create, _, **__):
PackageFactory(company=self)
The company=self
kwarg stops the recursion and the factory is properly created with the desired default package attribute.
Upvotes: 4