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.