From Europython 2016
Mocking is a valuable technique for writing tests but mocking effectively is often a stumbling block for many developers and can raise questions about its overall value as a technique.
The audience will have some familiarity with unit testing and may have tried mocking before, but some introduction will be provided for those who haven’t. We will look at some features and techniques of Python’s unittest.mock library and cover some useful tips and common scenarios, so this will be useful to those who have some experience mocking but would like to do so more effectively.
Video: https://www.youtube.com/watch?v=Ahnw72diels
4. unittest.mock
>>> from unittest import mock
# Or, for Python <3.3
$ pip install mock
>>> import 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
10. 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
11. 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
disable it with unsafe=True
assert*
assret*
12. 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)
13. 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)
14. 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
15. How to use all this
1. Create / insert a mock
2. Set up inputs / environment
3. Run the code under test
4. Assert expectations met
16. patch
Get access to anything
>>> from unittest.mock import patch
>>> patch('requests.get')
<unittest.mock._patch object at 0x7f7a2af03588>
creates a MagicMock
17. 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
18. patch context manager
with patch('requests.get') as mock_get:
mock_get.return_value.json.return_value = {'followers': 100}
assert get_followers('somebody') == 100
20. 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
21. 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
22. 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.
23. 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'
24. 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.
25. 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()
27. 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
28. 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)
29. 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'
30. 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
32. 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
33. 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