Python Tales and Plone Stories

4teamwork

Construct Your Testing Data Using the Builder Pattern

Test data generation is always a hot topic when it comes to automated software testing. One of my first assignments at 4teamwork was to help with the testing. In the process I was browsing through the test suites of some big projects they are working on and noticed a lot of similar code related to data creation:

1
2
3
4
5
6
7
8
9
10
11
12
13
dossier = createContentInContainer(self.portal,
                                   'opengever.dossier',
                                   title='Dossier',
                                   checkConstraints=False)

test_file = NamedBlobFile("lorem ipsum", filename=u"test.txt")
document = createContentInContainer(dossier,
                                    'opengever.document',
                                    checkConstraints=False,
                                    title=u'Foobar',
                                    keywords=[],
                                    file=test_file)
transaction.commit()

This is very basic object creation code and I’m sure many Plone developers have written a fair amount of it. While this gets the job done, there is a lot of noise, that we don’t need to have in every test.

I decided to use a basic implementation of the Builder Pattern to hide the noise in specific Builder classes and make our tests shorter and more expressive at the same time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class DossierBuilder(object):

    def __init__(self, session):
        self.arguments = {"checkConstraints": False}

    def titled(self, title):
        self.arguments["title"] = title
        return self

    def within(self, container):
        self.container = container
        return self

    def create(self):
        obj = createContentInContainer(self.container,
                                       'opengever.dossier',
                                        **self.arguments)
        transaction.commit()
        return obj


class DocumentBuilder(object):

    def __init__(self, session):
        self.arguments = {"checkConstraints": False,
                          "keywords": []}

    def titled(self, title):
        self.arguments["title"] = title
        return self

    def within(self, container):
        self.container = container
        return self

    def attach_file_containing(self, content, name=u"test.txt"):
        file_ = NamedBlobFile(data=content, filename=name)
        self.arguments["file"] = file_
        return self

    def create(self):
        obj = createContentInContainer(self.container,
                                       'opengever.document',
                                       **self.arguments)
        transaction.commit()
        return obj

We can now use our Builder classes inside our test-case.

1
2
3
4
5
from builders import DossierBuilder, DocumentBuilder

dossier = DossierBuilder().within(self.portal).titled(u"Dossier").create()
document = DocumentBuilder().within(dossier).titled(u'Foobar') \
    .attach_file_containing("lorem ipsum").create()

Already a big step forward! We have only defined two Builder classes, and we already notice some duplication among them. Defining a base class gives us a place to store the shared behavior:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class DexterityBuilder(object):

    def __init__(self, session):
        self.arguments = {"checkConstraints": False}

    def within(self, container):
        self.container = container
        return self

    def titled(self, title):
        self.arguments["title"] = title
        return self

    def create(self):
        obj = self.create_object()
        transaction.commit()
        return obj

class DossierBuilder(DexterityBuilder):

    def create_object(self):
        return createContentInContainer(self.container,
                                        'opengever.dossier',
                                        **self.arguments)

class DocumentBuilder(DexterityBuilder):

    def attach_file_containing(self, content, name=u"test.txt"):
        file_ = NamedBlobFile(data=content, filename=name)
        self.arguments["file"] = file_
        return self

    def create_object(self):
        return createContentInContainer(self.container,
                                        'opengever.document',
                                        **self.arguments)

Finally we hide the different Builder implementations and their instantiation behind a function. This allows us to change the Builders without touching all the tests that use them:

1
2
3
4
5
6
7
def Builder(name):
    if name == "dossier":
        return DossierBuilder()
    elif name == "document":
        return DocumentBuilder()
    else:
        raise ValueError("No Builder for %s" % name)

And the final code inside the test-cases looks like:

1
2
3
4
5
6
7
8
9
from builders import Builder

dossier = Builder("dossier") \
    .within(self.portal) \
    .titled(u"Dossier").create()
document = Builder("document") \
    .within(dossier) \
    .titled(u'Foobar') \
    .attach_file_containing("lorem ipsum").create()

Check out this gist to view our current builders.py module. Let me know what you think and leave a comment.

Comments