Search code examples
pythonunit-testingfactory-boy

Python: Factory Boy to generate List of length specified on object creation


I'm trying to use Factoryboy to create a list in an object of the length specified when created.

I can create the list, but every attempt to create a list with the length specified causes issues due to the lazy nature of the provided length/size.

This is what I have so far:

class FooFactory(factory.Factory):

    class Meta:
        model = command.Foo

    foo_uuid = factory.Faker("uuid4")
    bars = factory.List([
        factory.LazyAttribute(lambda o: BarFactory()
        for _ in range(3))
    ])

This will create a list of 3 random Bars. I have tried using a combination of Params and exclude, but because range expects an Int, and the int won't be lazily loaded until later, it causes an error.

I would like something similar to how one to many relationships are generated with post_generation ie.

foo = FooFactory(number_of_bars=5)

Anyone had any luck with this?


Solution

  • Main solution

    Two things are needed for this: parameters and LazyAttribute (the links point to their documentation, for more detail).

    Parameters are like factory attributes that are not passed to the instance that will be created. In this case, they provide a way to parametrize the length of the list of Bars.

    But in order to use parameters to customize a field in the factory, we need to have access to self, that is, the instance being built. We can achieve that with LazyAttribute, which is a declaration that takes a function with one argument: the object being built. Just what we needed.

    So the snippet in the question could be re-written as follows:

    class FooFactory(factory.Factory):
        class Meta:
            model = command.Foo
    
        class Params:
            number_of_bars = 1
    
        foo_uuid = factory.Faker("uuid4")
        bars = factory.LazyAttribute(lambda self: [BarFactory()] * self.number_of_bars)
    

    And used like this:

    foo = FooFactory(number_of_bars=3)
    

    If the number_of_bars argument is not provided, the default of 1 is used.

    Drawbacks

    Sadly, there is a some limitation to what we can do here.

    The preferred way to use a factory in the definition of another factory is via SubFactory. That is preferred for two reasons:

    1. it respects the build strategy used for the parent factory
    2. it collects extra keyword arguments to customize the subfactory

    The first one means that if we used SubFactory to build a Bar in FooFactory and called FooFactory with FooFactory.create or FooFactory.build, the Bar subfactory would respect that and use the same strategy. In summary, the build strategy only builds an instance, while the create strategy builds and saves the instance to the persistent storage being used, for example a database, so respecting this choice is important. See the docs for more details.

    The second one means that we can directly customize attributes of Bar when calling FooFactory. For example:

    foo = FooFactory(bar__id=2)
    

    would set the id of the bar of foo to be 2 instead of what the Bar subfactory would generate by default.

    But I could not find a way to use SubFactory and a dynamic length via Params. There is no way, as far as I know, to access the value of a parameter in a context where FactoryBoy expects a SubFactory. The problem is that the declarations that give us access to the object being built always expect a final value to be returned, not another factory to be called later. This means that, in the example above, if we write instead:

    class FooFactory(factory.Factory):
        # ... rest of the factory
        bars = factory.LazyAttribute(lambda self: [factory.SubFactory(BarFactory)] * self.number_of_bars)
    

    then calling it like

    foo = FooFactory(number_of_bars=3)
    

    would result in a foo that has a list of 3 BarFactory in foo.bars instead of a list of 3 Bars. And using SelfAttribute, which is a way to reference another attribute of the instance being built, doesn't work either because it is not evaluated before the rest of the expression in a declaration like this:

    class FooFactory(factory.Factory):
        # ... rest of the factory
        bars = factory.List([factory.SubFactory(BarFactory)] * SelfAttribute("number_of_bars"))
    

    That raises TypeError: can't multiply sequence by non-int of type 'SelfAttribute'. A possible workaround is to call BarFactory beforehand and pass it to FooFactory:

    number_of_bars = 3
    bars = BarFactory.create_batch(number_of_bars)
    foo = FooFactory(bars=bars)
    

    But that's certainly not as nice.

    Another one that I found out recently is RelatedFactoryList. But that's still experimental and it doesn't seem to have a way to access parameters. Additionally, since it's generated after the base factory, it also might not work if the instance constructor expects that attribute as an argument.