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: