Search code examples
pythondjangofactory-boy

FactoryBoy - nested factories / max depth?


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)

Solution

  • 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!