This post covers one final technique that’s used in the Hypothesis tests: matcher objects.

Matcher objects are a way to make complex assertions easier to read and write, make the intents of tests clearer, and reduce repetition between test methods. If you find yourself writing complex assertions that obscure the intent of your test, and especially if you find yourself repeating similar complex assertions in multiple tests, consider using a matcher.

Example

This test for GroupCreateController() tests that posting the create new group form returns a redirect:

def test_post_redirects_if_form_valid(controller):
    result = controller.post()

    assert isinstance(result, httpexceptions.HTTPSeeOther)
    assert result.location == '/g/abc123/fake-group'

This and every other test that wants to assert that something returns a particular kind of redirect (302, 303…) needs to repeat two assertions: that the correct type of Pyramid redirect exception is returned and that the exception has the correct location value.

This test can be simplified by using the redirect_303_to matcher:

def test_post_redirects_if_form_valid(controller, matchers):
    assert controller.post() == matchers.redirect_303_to('/g/abc123/fake-group')

This assertion will only pass if controller.post() returns an HTTPSeeOther with the correct location. A three-line test becomes one line, even with this very simple matcher you can save a lot of lines of test code when this pattern is repeated in every test that needs to check for a redirect.

How matcher objects work

Matcher classes are simply classes that implement Python’s __eq__() magic method.

Normally an object instance of a Python class isn’t equal to any other object except itself - == comparisons will always evaluate to False:

>>> class Foo(object):
...     pass
...
>>> foo_1 = Foo()

>>> # The object is equal to itself:
>>> foo_1 == foo_1
True

>>> # But the object isn't equal to any other object:
>>> foo_2 = Foo()
>>> foo_1 == foo_2
False
>>> foo_1 == 23
False
>>> foo_1 == "anything"
False

But you can customize the behavior of == by providing an __eq__() method in your class:

>>> class Foo(object):
...     def __eq__(self, other):
...         return self.location == other.location
...
>>> foo_1 = Foo()
>>> foo_1.location = "/example"

>>> # The object is equal to any other object that has the same `location`:
>>> foo_2 = Foo()
>>> foo_2.location = "/example"
>>> foo_1 == foo_2
True

>>> # Other comparison operators such as `in` also work:
>>> foo_1 in [foo_2, 23, "bar"]

>>> # But the object is _not_ equal to objects with a different `location`:
>>> foo_3 = Foo()
>>> foo_3.location = "/different"
>>> foo_1 == foo_3
False

Whenever it comes across a == expression Python calls the __eq__() method of the object on the left hand side of the expression. The __eq__() method returns True or False and this becomes the result of the == expression. Other standard comparison operators such as in also work with __eq__().

A matcher class is simply a class with an __eq__() method, here’s the redirect_303_to matcher that we used above:

class redirect_303_to(Matcher):
    """Matches any HTTPSeeOther redirect to the given URL."""

    def __init__(self, location):
        self.location = location

    def __eq__(self, other):
        if not isinstance(other, httpexceptions.HTTPSeeOther):
            return False
        return other.location == self.location

So when you create a redirect_303_to object with matchers.redirect_303_to('/g/abc123/fake-group'), in == comparisons that matcher object will be equal to any HTTPSeeOther object with the same location, and so the matcher can be used in assertions:

assert controller.post() == matchers.redirect_303_to('/g/abc123/fake-group')

The Matcher class that redirect_303_to inherits from simply provides an implementation of the __ne__() method, which Python calls to evaluate != and <> expressions. Any class that implements __eq__() should also implement __ne__(), by inheriting from Matcher matcher classes don’t have to implement __ne__() themselves.

Where to find the available matcher classes

To use a matcher a test method has a parameter named matchers:


def test_post_redirects_if_form_valid(controller, matchers):
    assert controller.post() == matchers.redirect_303_to('/g/abc123/fake-group')

matchers is a pytest fixture that contains all of the matcher classes defined in matchers.py as attributes (it’s the matchers.py module, made available as a fixture).

Of course a matcher class is just a class with an __eq__() method and a test module can just contain its own matcher classes, but if a matcher class seems like it might be useful across multiple test modules then consider putting it in matchers.py to make it available via the matchers fixture.

Another example

== comparisons and __eq__() are used under-the-hood in a lot of places in Python. For example the mock library’s assert_called_once_with() method uses it to test whether the argument that the mock method was called with matches the test’s expected argument, so you can pass a mock object as the expected argument. Matchers really come into their own when used in this way:

def test_it_fetches_the_groups_from_the_database(_fetch_groups,
                                                 group_pubids,
                                                 matchers):
    execute()

    _fetch_groups.assert_called_once_with(matchers.unordered_list(group_pubids))

Without the unordered_list matcher the test would have had to assert that _fetch_groups was called exactly once, that it was called with exactly one argument, and then implement a 6-line algorithm to check that the argument contained exactly those values in group_pubids but regardless of order (using sets would not implement this correctly). With a matcher the test can express this all in one line.

Matcher objects and value objects

I mentioned value objects in my post about When and When Not to Use Mocks. Value objects are simple, immutable objects that implement __eq__() and __ne__() and that are used in production code (not just in tests). Code can be designed to enable isolated unit testing without mocks by using value objects that are so simple that they don’t need to be mocked.

The mock library’s mock.call is an example of a value object - two call objects with the same values are equal:

>>> import mock
>>> call_1 = mock.call(1, 2, 3)
>>> call_2 = mock.call(1, 2, 3)
>>> call_1 == call_2
True

Every time you call a mock object it adds a call object to its call_args_list:

>>> my_mock = mock.MagicMock()
>>> my_mock("foo")
>>> my_mock.call_args_list
[call('foo')]

Because call objects are value objects you can create your own call objects and compare them to the ones in a mock’s call_args_list using standard Python comparison operators like in and ==:

>>> mock.call("foo") in my_mock.call_args_list
True
>>> mock.call("bar") in my_mock.call_args_list
False
>>> mock.call("foo") == my_mock.call_args_list[0]
True
>>> my_mock.call_args_list == [mock.call("foo")]
True

This can be useful when making complex assertions about how a mock was called, especially when multiple calls are involved and their order is important.

If your code uses value objects then your tests may not need to use matcher objects as the tests can just use the value object classes directly. Your tests may still need matcher objects if they want to do some alternative comparison instead of the one implemented by the value object’s __eq__() method (comparisons like the unordered list comparison above), or if the code under test doesn’t use value objects.

All posts tagged “Python Unit Tests at Hypothesis”: