FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.49k stars 392 forks source link

DjangoModelFactory with mixins #165

Open dedy-purwanto opened 9 years ago

dedy-purwanto commented 9 years ago

Hi guys, I was wondering how to write factories that shares the same attributes, such as created_by and modified_by. Suppose I have 2 models: Car and Manufacturer which has these 2 attributes, and in the Factory classes I wrote something like these:

class Car(models.Model):
    title = models.CharField()
    created_by = models.ForeignKey(User)
    modified_by = models.ForeignKey(User)

class Manufacturer(models.Model):
    title = models.CharField()
    created_by = models.ForeignKey(User)
    modified_by = models.ForeignKey(User)

class UserFactory(DjangoModelFactory)
    FACTORY_FOR = User

class UserStampMixin(object):
    @lazy_attribute
    def created_by(self): return UserFactory()

    @lazy_attribute
    def modified_by(self): return UserFactory()

class CarFactory(UserStampMixin, DjangoModelFactory)
    FACTORY_FOR = Car
    title = Sequence(lambda n: "Car %03d" % n)

class ManufacturerFactory(UserStampMixin, DjangoModelFactory)
    FACTORY_FOR = Manufacturer
    title = Sequence(lambda n: "Manufacturer %03d" % n) 

When accessing CarFactory.attributes() and ManufacturerFactory.attributes(), I get only {'title': '<sequence>'}, so all my factory classes are throwing error because user stamp can't be null. Tried with several different ways but the results are just the same. Does Factory Boy support mixins?

dedy-purwanto commented 9 years ago

I somehow solved it by overriding declarations in my mixin class, I'm not sure if it's the best solution.

class UserStampMixin(object):
    @classmethod
    def declarations(cls, extra_defs=None):
        extra_defs = extra_defs or {}
        user = UserFactory()
        extra_defs.update(created_by=user, modified_by=user)
        return getattr(cls, factory.base.CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs)
rbarrois commented 9 years ago

Hi,

Have you tried putting the mixin after DjangoModelFactory?

If this works, I'll add it to the recipes section of the docs :)

dedy-purwanto commented 9 years ago

I'm not sure if it's possible to put it after DjangoModelFactory since all the mixin's method will only be called after the class on the left, so it will not work, I tried various ways to inject these extra attributes to make it work when the mixin is placed on the right, none of them works.

Anyway, here's the refined version which can accept user's arguments instead of alway overriding them by whatever specified in the mixin:

class UserStampMixin(object):
    @classmethod
    def _get_extra(self):
        return dict(
            created_by=UserFactory(),
            modified_by=UserFactory(),
            )

    @classmethod
    def declarations(cls, extra_defs=None):
        extra = extra_defs or {}
        # Don't override fields if passed by the user
        extra.update({f: val for (f, val)
                in cls._get_extra().items() if f not in extra.keys()})
        return super(UserStampMixin, cls).declarations(extra)
aalvrz commented 6 years ago

Was there any consensus on what is the best way to implement this?

danihodovic commented 4 years ago

Was there any consensus on what is the best way to implement this?

I'm also curious

rbarrois commented 4 years ago

@danihodovic the solution should be using simple inheritance with a class Meta: abstract = True in factories. An example follows:

import collections
import factory
import factory.fuzzy

Car = collections.namedtuple('Car', ['brand', 'plate', 'mileage'])
Bus = collections.namedtuple('Bus', ['brand', 'plate', 'capacity'])

class VehicleFactory(factory.Factory):
    class Meta:
        abstract = True

    brand = factory.Iterator(['peugeot', 'renault', 'delorean'])
    plate = factory.fuzzy.FuzzyText(length=7)

class CarFactory(VehicleFactory):
    class Meta:
        model = Car
    mileage = factory.fuzzy.FuzzyInteger(0, 10000)

class BusFactory(VehicleFactory):
    class Meta:
        model = Bus
    capacity = factory.fuzzy.FuzzyInteger(20, 50)

print(BusFactory())
print(CarFactory())