Basic tutorial

builders is intended to facilitate test data creation and achieves it per two major capabilities:

Describing data model

Data models are commonly considered as large trees of crossreferenced objects.

For example, to describe a decomposition of a convenient automobile one might draw something like that:

digraph car {
car [shape=box];
car -> engine;
car -> body;
car -> gearbox;
car -> wheels [style=dotted];
wheels -> wheel_A;
wheels -> wheel_B;
wheels -> wheel_C;
wheels -> wheel_D;
body -> left_seat;
body -> right_seat;
}

The diagram declares that car consists of engine, gearbox, body and a set of wheels, completely ommiting the properties of each object.

Same can be described with builders as follows:

from builders.construct import Unique, Collection, Random

class Engine:
    hp = 100
    type = 'steam'

class Gearbox:
    gears = 4
    type = 'manual'

class Seat:
    material = 'leather'

class Body:
    color = 'blue'
    left_seat = Unique(Seat)
    right_seat = Unique(Seat)

class Wheel:
    size = 14
    threading = 'diagonal'
    type = 'winter_spiked'

class Car:
    make = 'ford'
    year_of_make = Random(1990, 2005)

    engine = Unique(Engine)
    gearbox = Unique(Gearbox)
    body = Unique(Body)
    wheels = Collection(Wheel, number=4)

The example is mostly self-describing. However, note:

  • each data model type has its own python class as a representation
  • default attribute values are given in the classes in primitives
  • references to other model types are declared via Construct attributes
  • there is no explicit root element or mandatory base classes

Building model instance

Long story short, building the model is as easy as:

from builders.builder import Builder

my_car = Builder(Car).build()

isinstance(my_car, Car)  # True
my_car.engine.hp == 100  # True
len(my_car.wheels)  # 4
type(my_car.wheels)  # list
my_car.wheels[0] == my_car.wheels[1]  # False, these are different wheels
1990 <= my_car.year_of_make <= 2005  # True, exact value of year_of_make varies

How this works? Builder recursevily walks over the tree starting with Car and instantiates model classes.

When a class instance is created, each attribute that is a Construct has its build method called. The resulting value is then assigned to that attribute of a built instance.

The Unique builds a single new instance of given type thus performing recursion step. Collection builds a number of new instances and puts them in a list.

There are several other useful constructs:

All the built-in constructs can be found at builders.construct. Custom constructs may be derived from builders.construct.Construct.

Modifying a tree

To build non-default model (and thats what you need most of the time) just apply some Modifiers to the tree like this:

from builders.modifiers import InstanceModifier, NumberOf

my_car = Builder(Car).withA(NumberOf(Car.wheels, 5)).build()

len(my_car.wheels)  # 5, we told it to be so

my_car = Builder(Car).withA(InstanceModifier(Seat).thatSets(material='fabric')).build()

my_car.body.left_seat.material  # 'fabric'
my_car.body.right_seat.material  # 'fabric'

The withA method accepts a number of modifiers and returns same Builder for the sake of chaining:

from builders.modifiers import InstanceModifier, NumberOf

Builder(Car).withA(NumberOf(Car.wheels, 5)).withA(InstanceModifier(Engine).thatSets(hp='over_9000')).withA(InstanceModifier(Body).thatSets(color='red')).build()

Obviously, configured builder can be used again to produce a another one similar car.

Useful built-in modifiers are:

All the built-in modifiers can be found in builders.modifiers.