Search code examples
pythondjangofactory-boy

factory_boy: add several dependent objects


I'm using factory_boy to replace fixtures in a Django app. I have a Product model that should have many Offers and Merchants.

#models.py
class Product(models.Model):
    name = models.CharField()

class Merchant(models.Model):
    product = models.ForeignKey(Product)
    name = models.CharField()

class Offer(models.Model):
    product = models.ForeignKey(Product)
    price = models.DecimalField(max_digits=10, decimal_places=2)

I want a factory that creates a Product with several Merchants and several Offers.

#factories.py
import random
from models import Offer, Merchant, Product

class OfferFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = Offer

    product = factory.SubFactory(ProductFactory)
    price = random.randrange(0, 50000, 1)/100.0


class MerchantFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = Merchant

    product = factory.SubFactory(ProductFactory)
    name = factory.Sequence(lambda n: 'Merchant %s' % n)
    url = factory.sequence(lambda n: 'www.merchant{n}.com'.format(n=n))

 class ProductFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = Product 

    name = "test product"
    offer = factory.RelatedFactory(OfferFactory, 'product')
    offer = factory.RelatedFactory(OfferFactory, 'product') # add a second offer
    offer = factory.RelatedFactory(OfferFactory, 'product') # add a third offer
    merchant = factory.RelatedFactory(MerchantFactory, 'product')
    merchant = factory.RelatedFactory(MerchantFactory, 'product') # add a second merchant
    merchant = factory.RelatedFactory(MerchantFactory, 'product') # add a third merchant

But when I use ProductFactory to create a Product, it only has one offer and one merchant.

In [1]: from myapp.products.factories import ProductFactory

In [2]: p = ProductFactory()

In [3]: p.offer_set.all()
Out[3]: [<Offer: $39.11>]

How do I set up a ProductFactory to have more than one dependent of a particular type?


Solution

  • To be able to specify the number of related objects in parent factory:

    models.py

    class Company(models.Model):
        name = models.CharField(max_length=255)
    
    
    class ContactPerson(models.Model):
        name = models.CharField(max_length=255)
        company = models.ForeignKey(Company, on_delete=CASCADE, related_name='contacts')
    

    factories.py

    class CompanyFactory(factory.django.DjangoModelFactory):
        name = factory.Faker('company')
    
        class Meta:
            model = Company
    
        @factory.post_generation
        def add_contacts(self, create, how_many, **kwargs):
            # this method will be called twice, first time how_many will take the value passed
            # in factory call (e.g., add_contacts=3), second time it will be None
            # (see factory.declarations.PostGeneration#call to understand how how_many is populated)
            # ContactPersonFactory is therefore called +1 times but somehow we get right amount of objs
            at_least = 1
            if not create:
                return
            for n in range(how_many or at_least):
                ContactPersonFactory(contact=self)
    
    
    
    class ContactPersonFactory(factory.django.DjangoModelFactory):
        name = factory.Faker('first_name')
    
        class Meta:
            model = ContactPerson
    

    tests.py

    company = CompanyFactory(company_name='ACME ltd', add_contacts=4)
    print(repr(company.name), len(company.contacts.all()))
    company = CompanyFactory(company_name='ACME ltd')
    print(repr(company.name), len(company.contacts.all()))
    
    ---
    'ACME ltd' 4
    'ACME ltd' 1
    

    If you are ok with always just one child, the docs solution works well:

    models.py

    class CompanyFactory(factory.django.DjangoModelFactory):
        name = factory.Faker('company')
        whatever_really = factory.RelatedFactory('my_app.factories.ContactPersonFactory', 'contact')
    
        class Meta:
            model = Company
    

    note the full path to the related factory.

    tests.py

    company = CompanyFactory(company_name='ACME ltd')
    print(repr(company.name), len(company.contacts.all()))
    ---
    'ACME ltd' 1
    

    versions used

    $ pip freeze | egrep 'factory|Faker|Django'
    Django==2.0.4
    factory-boy==2.10.0
    Faker==0.8.13
    $ python -V
    Python 3.6.5