Tutorial¶
In the simplest case creating a factory requires one line of code, not counting the imports:
>>> from arv.factory.api import Factory
>>> factory = Factory(name="Bob")
From now on we can use the factory to create objects:
>>> obj = factory()
>>> obj
{'name': 'Bob'}
Objects created with the factory may look the same but are independent from each other:
>>> obj1 = factory()
>>> obj2 = factory()
>>> obj1, obj2
({'name': 'Bob'}, {'name': 'Bob'})
>>> obj1["name"] = "Alice"
>>> obj1, obj2
({'name': 'Alice'}, {'name': 'Bob'})
The factory we just created is a bit boring, objects always have the
same value for the name attribute. We can override an attribute’s
value when creating an object by passing the desired value as a
keyword argument:
>>> alice = factory(name="Alice")
>>> alice
{'name': 'Alice'}
The Factory class is schemaless so it can’t check if an
attribute is allowed or not neither its type. Factories created this
way will silently accept any keyword argument of any type:
>>> factory = Factory()
>>> eve = factory(name=42, age="Eve")
>>> eve
{'age': 'Eve', 'name': 42}
Another way to get objects with different values is using value generators:
>>> from arv.factory.api import gen
>>> factory = Factory(name=gen.Gen(["Bob", "Alice", "Eve"]))
>>> factory()
{'name': 'Bob'}
>>> factory()
{'name': 'Alice'}
>>> factory()
{'name': 'Eve'}
Note the use of Gen in the keyword argument name when creating
the factory. Gen creates a value generator from any iterable.
From now on we’ll refer to value generators simply as generators.
Not to confuse with python generators.
Warning
generators are allowed only when defining or creating a factory not when creating objects.
Warning
Trying to create new objects once the generator is
exhausted will raise an StopIteration exception:
>>> factory()
Traceback (most recent call last):
...
StopIteration
Finally, if an attribute of our objects is itself an object we can nest factories:
>>> pet_factory = Factory(
... name="Rocky",
... kind=gen.Gen(["dog", "cat", "snake"])
... )
>>> factory = Factory(
... name=gen.Gen(["Bob", "Alice"]),
... pet=pet_factory
... )
>>> factory()
{'pet': {'kind': 'dog', 'name': 'Rocky'}, 'name': 'Bob'}
>>> factory()
{'pet': {'kind': 'cat', 'name': 'Rocky'}, 'name': 'Alice'}
When nesting factories we can override attributes in the subobjects using the double underscore syntax:
>>> pet_factory = Factory(name="Rocky", kind="dog")
>>> factory = Factory(name="Bob", pet=pet_factory)
>>> factory(pet__name="Toby")
{'pet': {'kind': 'dog', 'name': 'Toby'}, 'name': 'Bob'}
The double underscore syntax only works when creating objects.
Creating many objects¶
Sometimes we need to create many objects. As a matter of convenience
factories define the many method so we can create as many objects
as required with just one call:
>>> factory = Factory(
... name=gen.Gen(["Bob", "Alice"]),
... age=42,
... )
>>> factory.many(2)
[{'age': 42, 'name': 'Bob'}, {'age': 42, 'name': 'Alice'}]
many also accepts generators as keyword arguments:
>>> factory = Factory(
... name=gen.Gen(["Bob", "Alice"]),
... age=42,
... )
>>> factory.many(2, age=gen.Gen([42, 39]))
[{'age': 42, 'name': 'Bob'}, {'age': 39, 'name': 'Alice'}]
Removing attributes¶
Ocasionally, in order to perform some testing, we may need to remove
some attribute from the generated object, that can be accomplished
specifying DELETE as the attribute’s value:
>>> from arv.factory.api import DELETE
>>> factory = Factory(name="Bob")
>>> empty = factory(name=DELETE)
>>> empty
{}
Metafactories¶
A metafactory is just a class whose instances are factories. We
could have called them just factory classes, but metafactories
sounds fancier. Factory is the base metafactory, any metafactory
must derivate from Factory or some of it’s subclasses.
The main use case for metafactories is code reuse:
>>> class MyFactory(Factory):
... defaults = {
... "name": "Bob",
... "age": 42,
... }
...
>>> factory = MyFactory()
>>> factory()
{'age': 42, 'name': 'Bob'}
In the previous example we don’t provide default values when creating
the factory, the defaults from MyFactory are used.
Default values can be overriden as usual when creating a factory and when creating objects:
>>> alice_factory = MyFactory(name="Alice")
>>> alice_factory(age=39)
{'age': 39, 'name': 'Alice'}
That’s useful when we need to create many factories with small variations in order to perform some specific testing.
In a metafactory definition we can also specify a factory or a metafactory as the default value for any attribute.
Pitfalls using metafactories¶
Consider the following example:
>>> class PetFactory(Factory):
... defaults = {
... "name": "Rocky",
... "kind": gen.Gen(["dog", "cat"]),
... }
...
>>> class PersonFactory(Factory):
... defaults = {
... "name": "Bob",
... "pet": PetFactory,
... }
...
>>> factory1 = PersonFactory()
>>> factory2 = PersonFactory()
>>> factory1()
{'pet': {'kind': 'dog', 'name': 'Rocky'}, 'name': 'Bob'}
>>> factory2()
{'pet': {'kind': 'cat', 'name': 'Rocky'}, 'name': 'Bob'}
Surprisingly the pet created by factory2 is a cat not a dog as we
may expect.
We specified PetFactory for the pet attribute so both
factory1 and factory2 use different pet factories:
>>> factory1._defaults["pet"] is factory2._defaults["pet"]
False
The reason for this behaviour is that the generator for the kind
attribute is created when the PetFactory is defined and the same
value will be shared by all the factories created from PetFactory,
so factory2, despite using a different PetFactory from
factory1, will consume the same generator for the kind
attribute.
>>> factory1._defaults["pet"]._defaults["kind"] is factory2._defaults["pet"]._defaults["kind"]
True
This can be illustrated creating a new pet factory:
>>> pet_factory = PetFactory()
>>> pet_factory()
Traceback (most recent call last):
...
StopIteration
the shared generator has been exhausted by the previous calls to
factory1 and factory2 and raises an exception.
What we need is delaying the creation of the generator until the
factory is created so each factory gets a different generator, this
can be done using the lazy class:
>>> class PetFactory(Factory):
... defaults = {
... "name": "Rocky",
... "kind": gen.lazy(gen.Gen, ["dog", "cat"]),
... }
...
>>> class PersonFactory(Factory):
... defaults = {
... "name": "Bob",
... "pet": PetFactory,
... }
...
>>> factory1 = PersonFactory()
>>> factory2 = PersonFactory()
>>> factory1()
{'pet': {'kind': 'dog', 'name': 'Rocky'}, 'name': 'Bob'}
>>> factory2()
{'pet': {'kind': 'dog', 'name': 'Rocky'}, 'name': 'Bob'}
Notice that lazy takes a callable and its arguments, not an actual
generator. Passing a generator, or any other non callable object, will
raise a TypeError exception:
>>> gen.lazy(gen.Gen([1, 2,3]))
Traceback (most recent call last):
...
TypeError
Another potential pitfall is specifying a factory as the default value for an attribute:
>>> pet_factory = Factory(name="Rocky", kind=gen.Gen(["dog", "cat"]))
>>> class MyFactory(Factory):
... defaults = {
... "name": "Bob",
... "pet": pet_factory,
... }
...
>>> factory1 = MyFactory()
>>> factory2 = MyFactory()
>>> factory1()
{'pet': {'kind': 'dog', 'name': 'Rocky'}, 'name': 'Bob'}
>>> factory2()
{'pet': {'kind': 'cat', 'name': 'Rocky'}, 'name': 'Bob'}
>>> pet_factory()
Traceback (most recent call last):
...
StopIteration
In this example both factory1 and factory2 share the factory
pet_factory, so factory2 will continue creating pets from
where factory1 left off, and creating another pet will raise an
exception.
Notice that, in this example, using a generator for the kind
attribute is not a problem since it’s created when the factory is
created and will not be shared by any other factory. In fact using
lazy in that context will not work:
>>> pet_factory = Factory(
... name="Rocky",
... kind=gen.lazy(gen.Gen, ["dog", "cat"])
... )
>>> pet_factory()
{'kind': <arv.factory.generators.lazy object at 0x...>, 'name': 'Rocky'}
As a rule of thumb, when defining metafactories use lazily created generators and metafactories as default values. When creating a factory use generators and factories.
Creating other types of objects¶
In the examples we have seen so far the factories created dictionaries but usually we want to create other types of objects, instances of some class, a Django or SQLAlchemy model etc. That can be accomplished defining a new metafactory with a constructor class attribute. The value of that attribute must be a callable that accepts keyword arguments an returns an object of the intended type, a class is the natural choice but any callable can do:
>>> class MyClass(object):
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
>>> class MyFactory(Factory):
... defaults = {"name": "Bob", "age": 42}
... constructor = MyClass
...
>>> factory = MyFactory()
>>> obj = factory()
>>> type(obj)
<class 'MyClass'>
>>> obj.name
'Bob'
>>> obj.age
42
As we’d expect this works with nested factories too:
>>> class Pet(object):
... def __init__(self, name, kind):
... self.name = name
... self.kind = kind
...
>>> class Person(object):
... def __init__(self, name, pet):
... self.name = name
... self.pet = pet
...
>>> class PetFactory(Factory):
... defaults = {"name": "Rocky", "kind": "dog"}
... constructor = Pet
...
>>> class PersonFactory(Factory):
... defaults = {"name": "Bob", "pet": PetFactory}
... constructor = Person
...
>>> factory = PersonFactory()
>>> obj = factory()
>>> type(obj)
<class 'Person'>
>>> obj.name
'Bob'
>>> type(obj.pet)
<class 'Pet'>
>>> obj.pet.name
'Rocky'
>>> obj.pet.kind
'dog'
Persisting objects¶
Usually you’ll need to save the objects created by the factory to some
persistent storage in order to perform the testing. arv.factory
implements functionality to ease defining persistent metafactories.
Persistent factories are just factories that define the make
method. This method just creates and returns an object that has been
saved to the storage backend.
In order to avoid dependency problems arv.factory does no provide
any persistent factory by itself. The companion package
arv.factory_django defines a base metafactory for persisting
Django models:
>>> from arv.factory_django.api import DjangoFactory
>>> class MyModelFactory(DjangoFactory):
... defaults = {"name": "Bob"}
... constructor = MyModel
...
>>> factory = MyModelFactory()
>>> obj = factory.make()
Note
Creating a persisted object will automatically persist all persistable subobjects, if any.
In order to get a non persisted object just call the factory as usual:
>>> obj = factory()
make accepts keyword arguments as does the factory:
>>> obj = factory.make(name="Alice")
>>> obj.name
'Alice'
Persistent factories also define the make_many method, equivalent
to the many method but persisting the objects.
Builtin generators¶
In the examples of this tutorial we have used finite generators for
illustration purposes but in a real scenario we usually need
infinite generators so that an spurious StopIteration don’t
breaks our tests.
arv.factory defines some generators that may be useful when
defining factories. Take a look at the API documentation for a
complete list.
For the sake of the tutorial we will introduce the mkgen and
string generators.
mkgen¶
mkgen takes a function (any callable in fact) and its arguments
and creates an infinite generator that calls the function every time
the generator is consumed:
>>> def myfunction(a, b):
... return "a=%s b=%s" % (a, b)
...
>>> g = gen.mkgen(myfunction, 1, b=2)
>>> g.next()
'a=1 b=2'
>>> g.next()
'a=1 b=2'
A more useful example would be using a function that returns different values each time it’s called, for example a random number generator:
>>> from random import randint
>>> g = gen.mkgen(randint, 0, 100)
>>> g.next()
50
>>> g.next()
85
or the next method from an iterator:
>>> l = [1, 2, 3]
>>> g = gen.mkgen(iter(l).next)
>>> g.next()
1
>>> g.next()
2
This is how some of the generators are implemented.
string¶
string is a generator that creates string values from a format
specification and a counter generator:
>>> g = gen.string()
>>> g.next()
'0'
>>> g.next()
'1'
We can specify a format string when creating the generator:
>>> g = gen.string("pet_%02i")
>>> g.next()
'pet_00'
>>> g.next()
'pet_01'
Internally string uses the % operator, so we can use any
format specification supported by %.
Additionally we can specify a counter:
>>> g = gen.string(counter=[1, 4, 9])
>>> g.next()
'1'
>>> g.next()
'4'
>>> g.next()
'9'
A counter it’s just an iterable. In practice we’ll probably use some python generator in order to generate an infinite sequence of values, but as said, the only requirement for the counter is being iterable.
We can be more creative making the counter produce tuples:
>>> g = gen.string(format="%02i-%02i", counter=((1, 1), (1, 2), (3, 2)))
>>> g.next()
'01-01'
>>> g.next()
'01-02'
>>> g.next()
'03-02'