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?
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 Bar
s.
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.
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:
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 Bar
s.
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.