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

 

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