I previously alluded to a solution to the problem I have had with creating relative complex object structures for the purpose of testing. I've now released the Python version of this library under the name NonMockObjects, which is now available from the Python Cheeseshop (sort of a CPAN equivalent).

Full documentation is in the package, but here's a more conversational introduction: This library allows me to create layered test functions, which can build on each other. By way of demonstration, here's a couple of snippets of my real testing code. Here's the simplest case, where I create a new "text hunk", which is the root text unit of my system that comments, entries, and everything else will be built on:

@register
def texthunk(data, content = "Test Content%(inc)s",
             author = author, format = TEXT_HUNK_FORMATS.HTML):
    return make(TextHunk, all_args())

@register uses the Python decorator syntax to register this function with the object that centralizes all the access to the test functions. The content uses an automatic "unique ID" incrementer, accessed via %(inc)s, which is standard Python text interpolation syntax. (It's unique per run, not globally; so far this has been adequate.) author = author is the real magic of the system; author is another @registered function, and when you call texthunk, you get one of three choices:

  • Leave the author parameter blank, in which case the author function will be called to create a new author and the TextHunk gets that author.
  • Pass in a use_author={} dict, which will cause a new Author to be created, but allows you to pass in parameters to affect the author. This allows you to retain the flexibility of creating a new author easily, while allowing you to specify what may be one or two attributes that are important to this test. This works recursively; author itself has further references, and you can pass in use_author={'use_django_user': {'first': 'Bob'}} to make sure your Author has the first name "Bob". You want to minimize this because this introduces coupling, although this coupling may be mitigated in later releases with some support for forwarding parameters, allowing you to re-write the forwarding if something changes later.
  • Pass in a pre-existing author object with author=my_author, which completely skips creating an author object.

The upshot is that if I just want a text hunk, I call data.texthunk(), but I can trivially customize that text hunk further. (Usually with the "author" parameter as I test various permission things.)

("Make" is just a simple function that wraps a ".save()" call around the Django object creation; in many apps you may directly return the results of an object construction.)

On the flip side, when you write the test function, you don't need to worry about which of the three scenarios above is in play; you get an "Author" object and you do whatever with it.

So, the solution to my previous long list of objects that need to be created just to test whether, say, a new author can post a comment, is reduced to:

import testfuncs # this has all my test function declarations
import nonmockobjects

data = nonmockobjects.Objects() # I'm used to calling it 'data' from work

author = data.author()  # creates a 'new user' by default
entry = data.entry() # creates an entry; note the entry will use a new author
comment = data.add_comment_to_entry(author=author, entry=entry)
# Note the contents of the comment are defaulted elsewhere, and
# for this test I don't care what they are.

You could actually simplify that down to a comment = data.add_comment_to_entry() and then extract the relevant new author and entry, but I prefer to be explicit about it. (I may end up being wrong here, and introducing coupling, but it seems likely to me that a comment is always going to need an author and a target entry.)

The decoupling that this introduces comes from the fact that when you don't care about something, and you don't specify it, your test is no longer coupled to it. When you say entry = data.entry(), you don't have to care about any irrelevant future changes to what may constitute an entry; you'll continue to get a correctly-structure Entry object back from that call, regardless. And hopefully should any relevant changes occur, that will manifest as a breaking test; one could argue that if it doesn't that's a bug in the tests.

I've had months of experience with this structure, if you count my original Perl experience (which isn't as easy to use but has the same effects in the end), and so far it's stood up to everything I've thrown at it; I think this is because the creation function really are functions and you can do whatever you need to do with them. I think if I tried to implement this with some clever metadata or something it'd fail; you really need the functions.

And to be fair, the texthunk above is the simplest possible case. At work we've got some slightly more complicated code than this, but in practice complicated code in your test creation functions needs to be factored up into your objects. The final, factored creation functions are generally concerned with simple data massaging and structure management, almost more data than code, which you can see in following, my most complicated creation function, for entries. The purpose of this function is to massage the act of creating an entry to go through the exact same function that entries created by the user in the normal use of the system goes through, which in this case is not the default Django-provided constructor, but a class method I added. (One of the helpful side effects of this library can be to make it easier to go through the same functions in your test code that the user will go through, by making it much easier to match the interface of those functions, which may assume complicated data structures exist.)

@register
def entry(data,
          categories = category, # can take a list, too
          content = "Test Content %(inc)s",
          author = author,
          format = TEXT_HUNK_FORMATS.HTML,
          status = ENTRY_STATUSES.NORMAL,
          link = None,
          title = None,
          summary = None):
    # We process this to go through the new_entry class method.
    if isinstance(categories, Category):
        categories = [categories]
    args = {'status': status,
            'link': link,
            'title': title,
            'summary': summary,
            'content': content,
            'author': author.id,
            'format': format,
            'categories': [x.id for x in categories]}
    return Entry.new_entry(args)