Many-to-many relationship in factory_boy?
Asked Answered
B

2

22

I'm trying to test a many-to-many relationship between two Django models using factory_boy. The factory_boy documentation doesn't appear to discuss this and I'm having trouble figuring out what I'm doing wrong. When I run the first test, I get the error "AttributeError: 'Pizza' object has no attribute 'topping'". I get a similar error for the second test.

When I run the tests in the debugger, I can see a 'toppings' object but it don't understand how to get the name from it. Have I defined PizzaFactory's _prepare method properly? How do you access the name in one table from the other table when you have a many-to-many relationship?

Thanks.

models.py:

from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=50)

    def __unicode__(self):
        return self.name

class Pizza(models.Model):
    name = models.CharField(max_length=100)
    toppings = models.ManyToManyField(Topping)

    def __unicode__(self):
        return self.name

factories.py:

import factory
from models import Topping, Pizza

class ToppingFactory(factory.Factory):
    name = 'mushrooms'

class PizzaFactory(factory.Factory):
    name = 'Vegetarian'

    @classmethod
    def _prepare(cls, create, **kwargs):
        topping = ToppingFactory()
        pizza = super(PizzaFactory, cls)._prepare(create, **kwargs)
        pizza.toppings.add(topping)
        return pizza

tests.py

from django.test import TestCase
import factory
from app.models import Topping, Pizza
from app.factories import ToppingFactory, PizzaFactory

class FactoryTestCase(TestCase):

    def test_pizza_has_mushrooms(self):
        pizza = PizzaFactory()
        self.assertTrue(pizza.topping.name, 'mushrooms')

    def test_mushrooms_on_pizza(self):
        topping = ToppingFactory()
        self.assertTrue(topping.pizza.name, 'Vegetarian')
Bittner answered 24/1, 2013 at 22:26 Comment(0)
A
38

I believe you need to use the @factory.post_generation decorator:

class PizzaFactory(factory.Factory):
    name = 'Vegetarian'

    @factory.post_generation
    def toppings(self, create, extracted, **kwargs):
        if not create:
            # Simple build, do nothing.
            return

        if extracted:
            # A list of groups were passed in, use them
            for topping in extracted:
                self.toppings.add(topping)

Then you would call it in tests.py pizza = PizzaFactory.create(toppings=(topping1, topping2, topping3))

This is taken directly from https://factoryboy.readthedocs.org/en/latest/recipes.html.

Abrade answered 20/11, 2013 at 10:41 Comment(0)
L
7

Just use mixer instead:

from mixer.backend.django import mixer

# Generate toppings randomly
pizza = mixer.blend(Pizza, toppings=mixer.RANDOM)

# Set toppings
toppings = mixer.cycle(3).blend(Topping)
pizza = mixer.blend(Pizza, toppings=toppings)

# Generate toppings with name=tomato
pizze = mixer.blend(Pizza, toppings__name='tomato')

Easy, configurable, faster, schemaless, declarative and you are getting in some tests what you want.

Lobelia answered 30/4, 2014 at 12:24 Comment(3)
Supporting anecdote -- why I chose mixer... I was drawn to factory_boy initially. I liked how in factory_boy, for a given Factory you could call a method attributes() and get a dict back, good for use with dataset. But what's great about mixer is it helps you be DRY because of how it can do so much on-the-fly. That means less testing-code to change if your code-under-test changes in the future!Sorn
are you guys still supporting it?Trivalent
Last commit for mixer was in in march, 2022... RIPPastiness

© 2022 - 2024 — McMap. All rights reserved.