Chris Adams Chris Adams - 2 months ago 12
Python Question

How to I pass values into dependent models with Factory Boy in Django?

Im' working on an open source django web app, and I'm looking to use Factory Boy to help me setting up models for some tests, but after a few hours reading the docs and looking at examples, I think I need to accept defeat and ask here.

I have a Customer model which looks a bit like this:

class Customer(models.Model):

def save(self, *args, **kwargs):
if not self.full_name:
raise ValidationError('The full_name field is required')
super(Customer, self).save(*args, **kwargs)


user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
related_name='customer',
null=True
)

created = models.DateTimeField()
created_in_billing_week = models.CharField(max_length=9)
full_name = models.CharField(max_length=255)
nickname = models.CharField(max_length=30)
mobile = models.CharField(max_length=30, default='', blank=True)
gocardless_current_mandate = models.OneToOneField(
BillingGoCardlessMandate,
on_delete=models.SET_NULL,
related_name='in_use_for_customer',
null=True,
)


I am also using the standard Django User Model, from django.contrib.auth.

Here's my factory code:

class UserFactory(DjangoModelFactory):

class Meta:
model = get_user_model()


class CustomerFactory(DjangoModelFactory):

class Meta:
model = models.Customer

full_name = fake.name()
nickname = factory.LazyAttribute(lambda obj: obj.full_name.split(' ')[0])

created = factory.LazyFunction(timezone.now)
created_in_billing_week = factory.LazyAttribute(lambda obj: str(get_billing_week(obj.created)))

mobile = fake.phone_number()

user = factory.SubFactory(UserFactory, username=nickname,
email="{}@example.com".format(nickname))


In my case, I want to be able to generate a customer like so

CustomerFactory(fullname="Joe Bloggs")


And have the corresponding user generated, with the correct username, and email address.

Right now, I'm getting this error:

AttributeError: The parameter full_name is unknown. Evaluated attributes are {'email': '<factory.declarations.LazyAttribute object at 0x111d999e8>@example.com'}, definitions are {'email': '<factory.declarations.LazyAttribute object at 0x111d999e8>@example.com', 'username': <DeclarationWrapper for <factory.declarations.LazyAttribute object at 0x111d999e8>>}.


I think this is because I'm relying on a lazy attribute here in the customer, which isn't called before the user factory is created.

How should I be doing this if I want to be able to use a factory to create the Customer model instance, with a corresponding user as described above?

For what it's worth the full model is visible here on the github repo

Answer

In that case, the best way is to pick values from the customer declarations:

class CustomerFactory(DjangoModelFactory):

    class Meta:
        model = models.Customer

    full_name = factory.Faker('name')
    nickname = factory.LazyAttribute(lambda obj: obj.full_name.split(' ')[0])

    created = factory.LazyFunction(timezone.now)
    created_in_billing_week = factory.LazyAttribute(lambda obj: str(get_billing_week(obj.created)))

    mobile = factory.Faker('phone_number')

    user = factory.SubFactory(
        UserFactory,
        username=factory.SelfAttribute('..nickname'),
        email=factory.LazyAttribute(lambda u: "{}@example.com".format(u.username)))
    )

Also, use factory.Faker('field_name') to get random values for each instance: the line fullname = fake.name() in the class declaration is equivalent to:

DEFAULT_FULL_NAME = fake.name()

class CustomerFactory(DjangoModelFactory):
    full_name = DEFAULT_FULL_NAME

    class Meta:
        model = models.customer

Whereas factory.Faker('name') is equivalent to:

class CustomerFactory(DjangoModelFactory):
    full_name = factory.LazyFunction(fake.name)

    class Meta:
        model = models.customer

i.e fake.name() will provide a different name to each model built with this factory.