Managing mocks
The how, why and when of mocking in Python
Helen Sherwood-Taylor
EuroPython
21st July 2016
What is a mock?
- A fake object that replaces a real object
- Control and isolate the test's environment
Why?
- Deterministic tests
- Control the test inputs
- Faster test suites
unittest.mock
- also known as the mock library
- written by Michael Foord
- Added to Python in 3.3
- Rolling backport for earlier Pythons
- not just for unittest
>>> from unittest import mock
# Or, for Python <3.3
$ pip install mock
>>> import mock
Mock objects
>>> from unittest.mock import Mock
>>> mock_talk = Mock(conference='EuroPython')
>>> mock_talk.conference
'EuroPython'
>>> mock_talk.speaker
<Mock name='mock.speaker' id='139910592223552'>
>>> mock_talk.speaker.name
<Mock name='mock.speaker.name' id='139910592222208'>
>>> mock_talk.speaker.name = 'Helen'
>>> mock_talk.speaker.name
'Helen'
Properties of Mocks
Callable Mocks
>>> mock_talk.speaker.get_twitter_status()
<Mock name='mock.speaker.get_twitter_status()'>
>>> mock_talk.speaker.\
get_twitter_status.return_value = 'Eating pintxos'
>>> mock_talk.speaker.get_twitter_status()
'Eating pintxos'
return_value
Side effects
An exception
>>> mock_func = Mock()
>>> mock_func.side_effect = ValueError
>>> mock_func()
Traceback (most recent call last):
...
ValueError
A function
>>> mock_func.side_effect = lambda x: x*2
>>> mock_func(2)
4
Multiple side effects
A list of return values and exceptions
>>> mock_func.side_effect = [1, ValueError, 2]
>>> mock_func()
Traceback (most recent call last):
...
ValueError
>>> mock_func()
Traceback (most recent call last):
...
StopIteration
>>> mock_func()
1
>>> mock_func()
2
What was called?
-
assert_called_with(*args, **kwargs)
-
assert_called_once_with(*args, **kwargs)
-
assert_any_call(*args, **kwargs)
-
assert_has_calls((call(*args, **kwargs), ...))
-
assert_not_called()
>>> from unittest.mock import Mock, call
>>> mock_func = Mock()
>>> mock_func(1)
>>> mock_func(2)
>>> mock_func.assert_called_with(2)
>>> mock_func.assert_has_calls([call(1), call(2)])
Beware versions
-
assert_not_called
>>> mock_func = Mock()
>>> mock_func()
>>> mock_func.assert_not_called()
<Mock name='mock.assert_not_called()' id='139758629541536'>
python 3.5
>>> mock_func = Mock()
>>> mock_func()
>>> mock_func.assert_not_called()
...
AssertionError: Expected 'mock' to not have been called. Called 1 times.
python 3.4
assert safety
python 3.5
>>> from unittest.mock import Mock, call
>>> mock_func = Mock()
>>> mock_func.assert_called()
...
AttributeError: assert_called
- Detects non existent assertion calls
-
assert*
-
assret*
- disable it with unsafe=True
Matching call args
mock.ANY doesn't care
>>> from unittest.mock import Mock, ANY
>>> mock_func = Mock()
>>> mock_func(25)
>>> mock_func.assert_called_with(ANY)
Comparisons
Use comparison objects for more control
class MultipleMatcher:
def __init__(self, factor):
self.factor = factor
def __eq__(self, other):
return other % self.factor == 0
def __repr__(self):
return 'Multiple of {}'.format(self.factor)
>>> mock_func = Mock()
>>> mock_func(25)
>>> mock_func.assert_called_with(MultipleMatcher(5))
>>> mock_func.assert_called_with(MultipleMatcher(4))
...
AssertionError: Expected call: mock_func(Multiple of 4)
Actual call: mock_func(25)
Call inspection
Lower level inspection of calls
-
called - was it called?
-
call_count - how many times?
-
call_args - args/kwargs of last call
-
call_args_list - args/kwargs of all calls
How to use all this
- Create / insert a mock
- Set up inputs / environment
- Run the code under test
- Assert expectations met
patch
Get access to anything
>>> from unittest.mock import patch
>>> patch('requests.get')
<unittest.mock._patch object at 0x7f7a2af03588>
creates a MagicMock
patch decorator
import requests
def get_followers(username):
response = requests.get(
'https://api.github.com/users/%s' % username
)
return response.json()['followers']
@patch('requests.get')
def test_get_followers(mock_get):
mock_get.return_value.json.return_value = {'followers': 100}
assert get_followers('somebody') == 100
mock is a parameter to the test method
>>> print(get_followers('helenst'))
25
patch context manager
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'followers': 100}
assert get_followers('somebody') == 100
patch: manual control
patcher = patch('requests.get')
mock_get = patcher.start()
mock_get.return_value.json.return_value = {'followers': 100}
assert get_followers('somebody') == 100
patcher.stop()
Where to patch
# github_utils.py
import requests
def get_followers(username):
response = requests.get(
'https://api.github.com/users/%s' % username
)
return response.json()['followers']
import github_utils
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'followers': 100}
# This will succeed.
assert github_utils.get_followers('somebody') == 100
Where to patch: 2
# file: github_utils2
from requests import get
def get_followers(username):
response = get(
'https://api.github.com/users/%s' % username
)
return response.json()['followers']
import github_utils2
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'followers': 100}
assert github_utils2.get_followers('somebody') == 100
AssertionError: 25 != 100
Where to patch: the answer
patch('github_utils2.get')
import github_utils2
with patch('github_utils2.get') as mock_get:
mock_get.return_value.json.return_value = {'followers': 100}
# This will succeed.
assert github_utils2.get_followers('somebody') == 100
Patch the imported path
ensure that you patch the name used by the system under test.
patch.object
Attach mocks to an object
class User:
def is_birthday(self):
# ...
pass
def greet(self):
if self.is_birthday():
return 'happy birthday'
else:
return 'hello'
user = User()
with patch.object(user, 'is_birthday', return_value=True):
# user.is_birthday is a MagicMock that returns True
# Check the outcome
assert user.greet() == 'happy birthday'
Mock the time
>>> patch("datetime.date.today").start()
...
TypeError: can't set attributes of built-in/extension type 'datetime.date'
date is written in C so you can't mock its attributes.
>>> patch('datetime.date').start()
<MagicMock name='date' id='140222796874696'>
But you can mock the whole thing.
Is it my birthday?
from datetime import date
def its_my_birthday():
today = date.today()
return today.month == 3 and today.day == 19
import birthday
@patch('birthday.date')
def test_birthday_3(mock_date):
mock_date.today.return_value = date(2016, 3, 19)
assert birthday.its_my_birthday()
freezegun
$ pip install freezegun
from freezegun import freeze_time
import birthday
@freeze_time('2016-03-19')
def test_birthday_4():
assert birthday.its_my_birthday()
Mock the filesystem
from unittest.mock import mock_open
open_mock = mock_open(
read_data='look at all my file contents'
)
with patch('__main__.open', open_mock, create=True):
with open('myfile') as file:
assert file.read() == 'look at all my file contents'
mock_open will help you out here.
But you can't iterate
Iterate a mock file
with patch('__main__.open', mock_open(), create=True) as open_mock:
mock_file = open_mock.return_value.__enter__.return_value
mock_file.__iter__.return_value = ([
'line one', 'line two'
])
with open('myfile') as file:
assert list(file) == ['line one', 'line two']
with open('myfile') as myfile:
for line in myfile:
print(line)
Mock a property
class Person:
@property
def is_birthday(self):
today = date.today()
return self.dob.month == today.month and self.dob.day == today.day
def greet(self):
return 'Happy birthday!' if self.is_birthday else 'Good morning!'
# Patching the object doesn't work.
>>> with patch.object(person, 'is_birthday', return_value=True):
... assert person.greet() == 'Happy birthday!'
...
AttributeError: <__main__.Person object at 0x7f9875ea73c8>
does not have the attribute 'is_birthday'
How to mock a property
with patch.object(
Person,
'is_birthday',
new_callable=PropertyMock,
return_value=True
):
assert person.greet() == 'Happy birthday!'
- use new_callable
- patch the class
Example: delay loop
def retry_with_delay(func):
delay = 1
while True:
try:
return func()
except DatabaseError:
time.sleep(delay)
delay *= 2
@patch('time.sleep')
def test_third_time_lucky(mock_sleep):
mock_func = Mock(side_effect=[DatabaseError, DatabaseError, 'Yay'])
result = retry_with_delay(mock_func)
assert mock_func.call_count == 3
mock_sleep.assert_has_calls([
call(1),
call(2),
])
assert result == 'Yay'
When to mock?
- Current time
say happy birthday to a user
- Simulated failure
(what happens when the disk is full?)
- Slowness
e.g. time.sleep
- Randomness
- Remote APIs
any data you want
What about the rest?
- Purity / Design
- Speed
- Pinpoint failures
- Realistic coverage
- Harder to read
- Harder to write
- Meaningless?
- Doesn't test inter-layer stuff
Should I mock the other layers?
YES
NO
Further reading
- Book: http://www.obeythetestinggoat.com/
- Talk: Gary Bernhardt - Fast Test, Slow Test
- https://docs.python.org/3/library/unittest.mock.html
Thank you!
Twitter: @helenst
Github: @helenst
http://helen.st/
https://github.com/helenst/managing_mocks
Managing Mocks
By Helen Sherwood-Taylor
Managing Mocks
For EuroPython 2016
- 2,733