FactoryBoy - nested factories / max depth?
Asked Answered
S

1

7

I am writing tests for a large Django application, as part of this process I am gradually creating factories for all models of the different apps within the Django project.

However, I've run into some confusing behavior with FactoryBoy where it almost seems like SubFactories have an max depth beyond which no instances are generated.

The error occurs when I try to run the following test:

    def test_subfactories(self):
        """ Verify that the factory is able to initialize """
        user = UserFactory()
        self.assertTrue(user)
        self.assertTrue(user.profile)
        self.assertTrue(user.profile.tenant)

        order = OrderFactory()
        self.assertTrue(order)
        self.assertTrue(order.user.profile.tenant)

The last line will fail (AssertionError: None is not true), running this test through a debugger reveals that indeed order.user.profile.tenant returns None instead of the expected Tenant instance.

There are quite a few factories / models involved here, but the layout is relatively simple.

The User (django default) and the Profile model are linked through a OneToOneField, which (after some trouble) is represented by the UserFactory and ProfileFactory

@factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = yuza_models.Profile
        django_get_or_create = ('user',)

    user = factory.SubFactory('yuza.factories.UserFactory')
    birth_date = factory.Faker('date_of_birth')
    street = factory.Faker('street_name')
    house_number = factory.Faker('building_number')
    city = factory.Faker('city')
    country = factory.Faker('country')
    avatar_file = factory.django.ImageField(color='blue')
    tenant = factory.SubFactory(TenantFactory)
@factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = auth_models.User

    username = factory.Sequence(lambda n: "user_%d" % n)
    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')

    email = factory.Faker('email')
    is_staff = False
    is_superuser = False
    is_active = True
    last_login = factory.LazyFunction(timezone.now)

    @factory.post_generation
    def profile(self, create, extracted):
        if not create:
            return
        if extracted is None:
            ProfileFactory(user=self)

The TenantFactory below is represented as a SubFactory on the ProfileFactory above.

class TenantFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = elearning_models.Tenant

    name = factory.Faker('company')
    slug = factory.LazyAttribute(lambda obj: text.slugify(obj.name))
    name_manager = factory.Faker('name')
    title_manager = factory.Faker('job')
    street = factory.Faker('street_name')
    house_number = factory.Faker('building_number')
    house_number_addition = factory.Faker('secondary_address')

The Order is linked to a User, but many of its methods call fields of its self.user.profile.tenant

class OrderFactory(factory.DjangoModelFactory):
    class Meta:
        model = Order

    user = factory.SubFactory(UserFactory)
    order_date = factory.LazyFunction(timezone.now)
    price = factory.LazyFunction(lambda: Decimal(random.uniform(1, 100)))
    site_tenant = factory.SubFactory(TenantFactory)
    no_tax = fuzzy.FuzzyChoice([True, False])

Again, most of the asserts in the test pass without failing, all separate factories are able to initialize fetch values from their immediate foreignkey relations. However, as soon as factories/models are three steps removed from each other the call will return None instead of the expected Tenant instance.

Since I was unable to find any reference to this behaviour in the FactoryBoy documentation its probably a bug on my side, but so far I've been unable to determine its origin. Does anyone know what I am doing wrong?

post_save method

def create_user_profile(sender, instance, created, **kwargs):
    if created:
        profile = Profile.objects.create(user=instance)
        resume = profile.get_resume()
        resume.initialize()


post_save.connect(create_user_profile, sender=User)
Southwestward answered 18/4, 2019 at 14:36 Comment(6)
Add the OrderFactory codeToscanini
Good catch, I fixed my example - thanks!Southwestward
Wild guess: try order.refresh_from_db() before the assertions. Also are there any custom save methods in those models? PostGeneration (your User.profile) includes a call to instance.save() before finishing. Do the tables for Profile and Tenant hold anything/what you would expect?Cassella
I can't shake the feeling that something is off with UserFactory.profile (yes I am aware that the fault appears to be with ProfileFactor.tenant). Does the test pass if you use the earlier created user instance: order = OrderFactory(user = user) ?Cassella
Thanks for your response! - including .refresh_from_db() before the assert did not pass - order = OrderFactory(user = user) does pass! - Order has a custom save method to set some datefields on save, but disabling this method did not change anything - Your comments about UserProfile did lead me to further examine the Profile object, there exists a post_save method (included in my post above) that created a Profile on User creation. I accounted for this signal by using the @factory.django.mute_signals decorater on both the UserFactory and the ProfileFactorySouthwestward
When I applied this decorater to the OrderFactory as well the tests passed! I had assumed that any calls on Order.user would trigger the UserFactory which had already been enclosed with the decorator - but evidently this wasnt enough.Southwestward
S
0

As I mentioned in a comment, I've discovered the source of the problem: the post-save method linked to the UserProfile (I've included the code in my post).

This post-save method created a Profile on User creation. I accounted for this signal by using the @factory.django.mute_signals decorater on both the UserFactoryand the ProfileFactory.

I had assumed that any calls on Order.user would trigger the UserFactory which had already been enclosed with the decorator, but this is not assumption proved to be wrong. Only when I applied the decorated to the OrderFactory as well did the tests pass.

Thus the @factory.django.mute_signals decorator should not just be used on factories that are affected by these signals, but also on any factory that is using those factories as a SubFactory!

Southwestward answered 29/4, 2019 at 16:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.