sentinel: Unique Objects for Tests
This post covers mock.sentinel, a small bonus feature of the mock library that’s used fairly often in the Hypothesis tests.
mock.sentinel
is a handy object for creating unique, opaque, named objects
for use in tests. You can access any attribute name on mock.sentinel
and each
one will return a different sentinel object:
>>> import mock
>>> mock.sentinel.foo
sentinel.foo
>>> mock.sentinel.bar
sentinel.bar
>>> mock.sentinel.whatever_name_you_want
sentinel.whatever_name_you_want
Each sentinel has a name
attribute, which is the name that you retrieved it
by:
>>> mock.sentinel.foo.name
'foo'
This name
is used to identify the sentinel whenever it’s printed out or
turned into a string. This lets you easily identify the object when it appears
in tracebacks in test failures or crashes, for example:
>>> print mock.sentinel.foo
sentinel.foo
>>> str(mock.sentinel.foo)
'sentinel.foo'
>>> repr(mock.sentinel.foo)
'sentinel.foo'
Two sentinels with the same name are considered equal. Actually, they’re the
same exact object - each time you access mock.sentinel.foo
it returns the
same one sentinel object named foo.
On the other hand, two sentinel objects with different names are not equal.
This equality is useful for making assertions in tests:
>>> mock.sentinel.foo == mock.sentinel.foo
True
>>> mock.sentinel.foo == mock.sentinel.bar
False
You can’t access any attribute or call any method on a sentinel, other
than name. Trying to access anything else will always raise AttributeError
.
Sentinel objects are the opposite of mock objects (on which
you can access any attribute name). This also differentiates sentinels from,
say, string objects, which have lots of methods for dealing with strings:
>>> # A string has an expandtabs() method, and many others:
>>> "foo".expandtabs()
'foo'
>>> # You can call any method on a mock:
>>> mock.MagicMock().expandtabs()
<MagicMock name='mock.expandtabs()' id='140262216783568'>
>>> # But you can't call *any* methods on a sentinel:
>>> mock.sentinel.foo.expandtabs()
Traceback (most recent call last):
...
AttributeError: '_SentinelObject' object has no attribute 'expandtabs'
What are sentinels used for?
Sentinels have one very specific use case: for testing that a method returns a specific object, or passes a specific object to another method as an argument, in cases when the code shouldn’t do anything else with that object. When the code under test shouldn’t get or set any attributes or call any methods on the object - in other words it should treat it like an opaque object. You use the equality property of sentinels to assert that the object returned, or passed to another method, was indeed the sentinel object as expected.
For example our test_push_appends_event_to_queue()
from the
earlier post about mock objects could have used a sentinel,
because EventQueue
shouldn’t do anything with the event object that we pass
it other than pushing it onto the queue:
def test_push_appends_event_to_queue(self):
event_queue = EventQueue()
event_queue.push(mock.sentinel.event)
assert list(event_queue.queue) == [mock.sentinel.event]
You’ll see sentinel
used fairly often in the Hypothesis tests.
The advantages of using sentinel
where possible instead of a mock, string,
or other stand-in object are:
-
It makes it explicit that the test is using a stand-in object.
For example the test above would work just fine if it used the string
"event"
instead ofmock.sentinel.event
, but then someone reading the test might think thatEventQueue
is a class that’s meant to work with strings as events, which is not the case.mock.sentinel
is never used in production code so whenever it’s used in tests it’s clear that it’s being used as a stand-in for a different type of object that would really be used in production. This isn’t as clear when tests use strings as stand-in objects. -
The test will fail if the code doesn’t treat the sentinel as opaque.
If the code under test tries to get or set any attribute or call any method on the sentinel it’ll raise
AttributeError
from the line that tried to access the attribute, so it’s easy to find where the bug is.When the tests expect something to be treated as an opaque object they can simply use a sentinel rather than say, trying to
assert
after the fact that nothing was accessed.
All posts tagged “Python Unit Tests at Hypothesis”:
-
May 2017
Matcher Objects in Python Tests -
Apr 2017
When and When Not to Use Mocks -
Mar 2017
usefixtures as a Class Decorator -
Mar 2017
The Problem with Mocks -
Mar 2017
sentinel: Unique Objects for Tests -
Mar 2017
Hypothesis’s patch Fixture -
Mar 2017
Python’s unittest.mock -
Feb 2017
Advanced pytest Fixtures -
Feb 2017
Basic pytest Fixtures -
Jan 2017
Parametrizing Python Tests -
Jan 2017
Testing that an Exception is Raised with pytest.raises -
Jan 2017
Arrange, Act, Assert -
Jan 2017
Writing Simple Python Unit Tests -
Jan 2017
Debugging Failing Tests with pytest -
Jan 2017
Running the Hypothesis Python Tests -
Jan 2017
Python Unit Tests at Hypothesis