Managing mocks
The how, why and when of mocking in Python
Helen Sherwood-Taylor
PyCon UK
19th September 2015
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
# Python 2.x, <3.3
$ pip install mock
>>> import mock
# Python 3.3+
>>> from unittest import mock
Mock objects
>>> from unittest.mock import Mock
>>> mock_talk = Mock(conference='PyCon UK')
>>> mock_talk.conference
'PyCon UK'
>>> 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'
>>> mock_talk.speaker.get_twitter_status()
<Mock name='mock.speaker.get_twitter_status()' id='139910592221312'>
>>> mock_talk.speaker.get_twitter_status.return_value = 'Eating cheese'
>>> mock_talk.speaker.get_twitter_status()
'Eating cheese'
Side effects
An exception
>>> mock_func = Mock()
>>> mock_func.side_effect = Exception
>>> mock_func()
Traceback (most recent call last):
...
Exception
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()
1
>>> mock_func()
Traceback (most recent call last):
...
ValueError
>>> mock_func()
2
>>> mock_func()
Traceback (most recent call last):
...
StopIteration
What was called?
-
assert_called_with(*args,**kwargs)
-
assert_called_once_with(*args, **kwargs)
-
assert_any_call(*args, **kwargs)
-
assert_has_calls(*args, **kwargs)
>>> 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)])
New in Python 3.5 - assert_not_called
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)
Matchers
Use matcher 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
Action / Assertion mechanics
- Create a mock
- Set up inputs / environment
- Run the code under test
- Assert expectations met
Direct creation
Mock another method on object under test
def test_greeting(self):
user = User()
user.is_birthday = Mock(return_value=True)
greeting = user.greet()
self.assertEqual(greeting, 'happy birthday')
patch
Get access to anything
>>> patch('requests.get')
<unittest.mock._patch object at 0x7f7a2af03588>
- Decorator
- Context manager
- Manual control
creates a MagicMock
patch decorator
import github3
def get_followers(username):
api_client = github3.GitHub()
return api_client.user(username).followers
class GitHubTest(unittest.TestCase):
@patch('github3.GitHub')
def test_get_followers(self, mockGitHub):
mockGitHub.return_value.user.return_value.followers = 20
followers = get_followers('helenst')
self.assertEqual(followers, 20)
mock is a parameter to the test method
patch context manager
import github3
def get_followers(username):
api_client = github3.GitHub()
return api_client.user(username).followers
class GitHubTest(TestCase):
def test_get_followers(self):
with patch('github3.GitHub') as mockGitHub:
mockGitHub.return_value.user.return_value.followers = 20
followers = get_followers('helenst')
self.assertEqual(followers, 20)
patch: manual control
import github3
def get_followers(username):
api_client = github3.GitHub()
return api_client.user(username).followers
class GitHubTest(TestCase):
def test_get_followers(self):
patcher = patch('github3.GitHub')
mockGitHub = patcher.start()
mockGitHub.return_value.user.return_value.followers = 20
followers = get_followers('helenst')
self.assertEqual(followers, 20)
patcher.stop()
Where to patch
import github3
def get_followers(username):
api_client = github3.GitHub()
return api_client.user(username).followers
import github_utils
class GitHubTest(unittest.TestCase):
@patch('github3.GitHub')
def test_get_followers(self, mockGitHub):
mockGitHub.return_value.user.return_value.followers = 200
followers = github_utils.get_followers('helenst')
self.assertEqual(followers, 200)
Where to patch: 2
from github3 import GitHub
def get_followers(username):
api_client = GitHub()
return api_client.user(username).followers
import github_utils
class GitHubTest(unittest.TestCase):
@patch('github3.GitHub')
def test_get_followers(self, mockGitHub):
mockGitHub.return_value.user.return_value.followers = 200
followers = github_utils.get_followers('helenst')
self.assertEqual(followers, 200)
AssertionError: 14 != 200
Where to patch: the answer
@patch('github_utils.GitHub')
import github_utils
class GitHubTest(unittest.TestCase):
@patch('github_utils.GitHub')
def test_get_followers(self, mockGitHub):
mockGitHub.return_value.user.return_value.followers = 200
followers = github_utils.get_followers('helenst')
self.assertEqual(followers, 200)
Patch the imported path
ensure that you patch the name used by the system under test.
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 3rd party APIs
any data you want
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(self, mock_sleep):
mock_func = Mock(side_effect=[DatabaseError, DatabaseError, 'Yay'])
result = retry_with_delay(mock_func)
self.assertEqual(result, 'Yay')
self.assertEqual(mock_func.call_count, 3)
mock_sleep.assert_has_calls([
call(1),
call(2),
])
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(self, mock_date):
mock_date.today.return_value = date(2015, 3, 19)
self.assertTrue(birthday.its_my_birthday())
Mock the filesystem
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 read only 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!'
>>> person = Person()
>>> person.is_birthday = True
Traceback (most recent call last):
...
AttributeError: can't set attribute
How to 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!'
person = Person()
with patch.object(
Person, 'is_birthday', new_callable=PropertyMock
) as mock_is_birthday:
mock_is_birthday.return_value = True
person.greet()
patch.object to the rescue!
What about the rest?
- Purity / Design
- Speed
- Pinpoint failures
- Realistic coverage
- Harder to read
- Harder to write
- Sometimes seem meaningless
- Doesn't test inter-layer stuff
Should I mock the other layers?
YES
NO
Thank you!
Twitter: @helenst
Github: @helenst
http://helen.st/
Managing Mocks
By Helen Sherwood-Taylor
Managing Mocks
For PyCon UK 2015
- 3,973