Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Managing Mocks

Managing Mocks

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

Helen Sherwood-Taylor

July 21, 2016
Tweet

More Decks by Helen Sherwood-Taylor

Other Decks in Programming

Transcript

  1. Managing mocks The how, why and when of mocking in

    Python Helen Sherwood-Taylor EuroPython 21st July 2016
  2. What is a mock? A fake object that replaces a

    real object Control and isolate the test's environment
  3. 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
  4. 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
  5. 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
  6. 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
  7. 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)])
  8. 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
  9. 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*
  10. 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)
  11. 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)
  12. 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
  13. 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
  14. patch Get access to anything >>> from unittest.mock import patch

    >>> patch('requests.get') <unittest.mock._patch object at 0x7f7a2af03588> creates a MagicMock
  15. 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
  16. 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
  17. 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
  18. 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.
  19. 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'
  20. 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.
  21. 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()
  22. 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()
  23. 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
  24. 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)
  25. 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'
  26. 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
  27. 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'
  28. 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
  29. 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
  30. Further reading Book: http://www.obeythetestinggoat.com/ Talk: Gary Bernhardt - Fast Test,

    Slow Test https://docs.python.org/3/library/unittest.mock.html