In a previous post on unit testing I described the motivation behind our recent move to pytest as a unit testing framework and the experiences we made in the early phase of the migration. Here, I want to highlight a more advanced issue we encountered during the arduous work of porting our domain model unit tests to the new pytest
based framework: The issue of testing complex, hierarchical data structures.

The @pytest.parametrized decorator is very useful for setting up simple test objects on the fly, but since the decorator is called at module load time, you may not be able to create complex test objects with it. Also, the syntax is fairly cumbersome to read with more complex object initializations.

To illustrate the problem, consider the following simple test class diagram [1]:

myentity

Suppose we want to perform tests on MyEntity instances. Suppose further that we need also properly initialized MyEntityParent, MyEntityChild and MyEntityGrandchild instances attached to our test object. The standard way to achieve this would be a pytest fixture looking like this:

However, if we now needed a differently configured MyEntity instance – say, with two MyEntityChild instances or a different id, we would end up with a lot of code duplication.

After some time playing around with various ideas on how to make the generation of complex test object trees more flexible, I had the idea to simply add one layer of indirection by defining test object factory fixtures for the individual test object classes like this:

When used as a fixture parameter in a test function (or another fixture), calling these factories with no arguments always returns an instance fully initialized with the default arguments defined by the factory fixture:

Very convenient. However, it is also straightforward to create customized test object fixtures using these factories:

There is one thing to keep in mind here: The default behavior of the test object factories is to create only one instance for each unique combination of positional and keyword arguments. So, in the example above, since the children attribute was not customized in the calls to the my_entity_child_fac factory, both MyEntityChild instances created end up with the exact same MyEntityGrandchild instance in their children list. If you absolutely need new instances, you can either pass unique arguments to the factory or call its new method.

On another, perhaps slightly esoteric, side note I want to point out that the code above has the unnerving effect of immediately generating a pylint warning in line 10, code number W0621, informing you that a name from the outer scope in line 4 was just redefined. Getting rid of this warning without disabling it manually or moving the test fixture into a separate conftest module turned out to be not all that easy; I eventually resorted to the following way of declaring my fixtures at the top of my test modules:

and use one of the ingenious pytest hooks to pre-process the module at test collection time (this needs to go somewhere pytest can see it, e.g. in a conftest module in the test package):

Again, I am happy to report that pytest gives me all the tools needed to cope with our sometimes complex testing scenarios and that I do not regret our decision to migrate to pytest at all.

Footnotes    (↵ returns to text)
  1. These classes are taken from the test suite of everest)