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

  1. Create / insert a mock
  2. Set up inputs / environment
  3. Run the code under test
  4. 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

  • 366
Loading comments...

More from Helen Sherwood-Taylor