Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Mock it right! A beginner’s guide to world of tests and mocks, Maciej Polańczyk

2,622 views

Published on

PyParis 2017
http://pyparis.org

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Mock it right! A beginner’s guide to world of tests and mocks, Maciej Polańczyk

  1. 1. Mock it right! A beginner’s guide to world of tests and mocks. Maciej Polańczyk maciej.polanczyk@stxnext.pl Mock, Patch
  2. 2. Schedule - Class to be tested - Basic test - Mock - Not the best mock - The best mock - spec_set - Good mock - spec - Mock requests.post - Patch - Patch.object - Patch & import - Patch - raise Exception - Patch - different return_value - Patch - method instead of return_value - Patch - nesting patches - Patch - TestCase - Patch - in setUp - Patch - environment variables https://docs.python.org/3/library/unittest.mock.html
  3. 3. Class to be tested import requests class MessageSender: _max_attempts = 3 def send(self, message): data = { 'from': message.sender, 'to': message.receiver, 'subject': message.subject, } for attempt in range(self._max_attempts): response = requests.post(url='http://example.com/email', data=data) if response.status_code == requests.codes.created: return True return False
  4. 4. Basic test from message_sender import MessageSender class MessageSenderTests(unittest.TestCase): def test_should_send_message(): # GIVEN message_sender = MessageSender() message_to_be_sent = self._get_mocked_message() # WHEN result = message_sender.send(message_to_be_sent) # THEN self.assertTrue(result) # assert correct arguments were passed to request.post
  5. 5. Mock Use mock when you can pass it
  6. 6. Mock - creation from unittest.mock import Mock # mock and attributes mock = Mock() print(mock.not_existing_field) <Mock name='mock.not_existing_field' id='4327526528'> from unittest.mock import Mock # mock and methods mock = Mock() print(mock.not_existing_method()) <Mock name='mock.not_existing_method()' id='4372615856'>
  7. 7. Mock - creation from unittest.mock import Mock # mock and attributes mock = Mock() mock.existing_field = 5 print(mock.existing_field) 5 from unittest.mock import Mock # mock and methods mock = Mock() mock.existing_method.return_value = 5 print(mock.existing_method()) 5
  8. 8. Mock - creation from unittest.mock import Mock # mock and attributes mock = Mock(existing_field=5) print(mock.existing_field) 5 from unittest.mock import Mock # mock and methods mock = Mock(existing_method=Mock(return_value=5)) print(mock.existing_method()) 5
  9. 9. Mock - assertions from unittest.mock import Mock # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock.existing_method() # in test method: mock.existing_method.assert_called_once_with()
  10. 10. Mock - assertions from unittest.mock import Mock # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock.existing_method(1) # in test method: mock.existing_method.assert_called_once_with(1)
  11. 11. Mock - assertions from unittest.mock import Mock # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock.existing_method(1) mock.existing_method(2) # in test method: mock.existing_method.assert_any_call(1)
  12. 12. Mock - assertions from unittest.mock import Mock # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock(3) mock.existing_method(1) mock.existing_method(2) print(mock.call_args_list) output from print: [call(3)]
  13. 13. Mock - assertions from unittest.mock import Mock # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock(3) mock.existing_method(1) mock.existing_method(2) print(mock.method_calls) output from print: [call.existing_method(1), call.existing_method(2)]
  14. 14. Mock - assertions from unittest.mock import Mock # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock(3) mock.existing_method(1) mock.existing_method(2) print(mock.mock_calls) output from print: [call(3), call.existing_method(1), call.existing_method(2)]
  15. 15. Mock - assertions from unittest.mock import Mock, call # in test method: mock = Mock() mock.existing_method.return_value = 5 # in source code: mock(3) mock.existing_method(1) mock.existing_method(2) # in test method: self.assertEqual(mock.mock_calls, [call(3), call.existing_method(1), call.existing_method(2)]) self.assertEqual(mock.existing_method.mock_calls, [call(1), call(2)])
  16. 16. Not the best mock from unittest.mock import Mock @staticmethod def _get_mocked_message(): message_mock = Mock( receiver='maciej.polanczyk@stxnext.pl', sender='maciej.polanczyk@test.com', subject='testing sending message' ) return message_mock
  17. 17. The best mock - spec_set from unittest.mock import Mock from message import Message @staticmethod def _get_mocked_message(): message_mock = Mock( spec_set=Message, receiver='maciej.polanczyk@stxnext.pl', sender='maciej.polanczyk@test.com', subject='testing sending message' ) return message_mock
  18. 18. Message class class Message: receiver = None sender = None subject = None
  19. 19. Message class def Message: def __init__(self, receiver, sender, subject): self.receiver = receiver self.sender = sender self.subject = subject
  20. 20. Good mock - spec import requests from unittest.mock import Mock @staticmethod def _get_success_response(): response_mock = Mock( spec=requests.Response, status_code=requests.codes.created ) return response_mock
  21. 21. Good mock - spec import requests from unittest.mock import Mock @staticmethod def _get_failure_response(): response_mock = Mock( spec=requests.Response, status_code=requests.codes.bad ) return response_mock
  22. 22. Basic test from message_sender import MessageSender class MessageSenderTests(unittest.TestCase): def test_should_send_message(): # GIVEN message_sender = MessageSender() message_to_be_sent = self._get_mocked_message() # WHEN result = message_sender.send(message_to_be_sent) # THEN self.assertTrue(result) # assert correct arguments were passed to request.post
  23. 23. Mock request.post import requests class MessageSender: _max_attempts = 3 def send(self, message): data = { 'from': message.sender, 'to': message.receiver, 'subject': message.subject, } for attempt in range(self._max_attempts): response = requests.post(url='http://example.com/email', data=data) if response.status_code == requests.codes.created: return True return False
  24. 24. Mock request.post class MessageSender: _max_attempts = 3 def __init__(): self._sender = ... def send(self, message): ... response = self._sender.post( url='http://example.com/email', data=data, ) ... class MessageSenderTests(unittest.TestCase): def test_should_send_message(self): # GIVEN message_sender = MessageSender() message_sender._sender = self._get_mocked_post_method() ... @staticmethod def _get_mocked_post_method(): post_method_mock = Mock( # spec_set=SenderClass return_value=MessageSenderTests._get_success_response() ) return post_method_mock
  25. 25. Patch Use patch when you can not pass mock
  26. 26. Patch from unittest.mock import patch from message_sender import MessageSender @patch('requests.post', autospec=True) def test_should_send_message(self, post_mock): # GIVEN post_mock.return_value = self._get_success_response() ... # THEN self.assertTrue(result) post_mock.assert_called_once_with( data={ 'from': 'maciej.polanczyk@test.com', ... }, url='http://example.com/email' )
  27. 27. Patch.object import requests from unittest.mock import patch from message_sender import MessageSender @patch.object(requests, 'post', autospec=True) def test_should_send_message(self, post_mock): # GIVEN post_mock.return_value = self._get_success_response() ... # THEN self.assertTrue(result) post_mock.assert_called_once_with( data={ ... }, url='http://example.com/email' )
  28. 28. Patch & imports from requests import post ... def send(self, message): ... response = post( url='http://example.com/email', data=data, ) ... from unittest.mock import patch import message_sender from message_sender import MessageSender @patch.object(message_sender, 'post', autospec=True) def test_should_send_message(self, post_mock): ... import requests ... def send(self, message): ... response = requests.post( url='http://example.com/email', data=data, ) … import requests from unittest.mock import patch from message_sender import MessageSender @patch.object(requests, 'post', autospec=True) def test_should_send_message(self, post_mock): ...
  29. 29. Patch - raise Exception import requests class MessageSender: _max_attempts = 3 def send(self, message): data = { 'from': message.sender, 'to': message.receiver, 'subject': message.subject, } for attempt in range(self._max_attempts): response = requests.post(url='http://example.com/email', data=data) if response.status_code == requests.codes.created: return True return False
  30. 30. Patch - raise Exception @patch.object(requests, 'post') def test_should_not_catch_exception(self, post_mock): # GIVEN post_mock.side_effect = RequestException( 'Expected exception from unit tests' ) message_sender = MessageSender() message_to_be_sent = self._get_example_message() # WHEN & THEN with self.assertRaisesRegex(RequestException, 'Expected exception'): message_sender.send(message_to_be_sent) post_mock.assert_called_once_with( data={ ... }, url='http://example.com/email' )
  31. 31. Patch - different return_value import requests class MessageSender: _max_attempts = 3 def send(self, message): data = { 'from': message.sender, 'to': message.receiver, 'subject': message.subject, } for attempt in range(self._max_attempts): response = requests.post(url='http://example.com/email', data=data) if response.status_code == requests.codes.created: return True return False
  32. 32. Patch - different return_value @patch.object(requests, 'post', autospec=True) def test_should_retry_sending_when_incorrect_status_received(self, post_mock): # GIVEN post_mock.side_effect = [self._get_failure_response(), self._get_failure_response(), self._get_failure_response(),] message_to_be_sent = self._get_example_message() message_sender = MessageSender() # WHEN result = message_sender.send(message_to_be_sent) # THEN self.assertFalse(result) expected_calls = [self._get_expected_call(), self._get_expected_call(), self._get_expected_call(),] self.assertEqual(post_mock.call_args_list, expected_calls) def _get_expected_call(self): return call( data = { 'from': 'maciej.polanczyk@test.com', 'subject': 'testing sending message', 'to': 'maciej.polanczyk@stxnext.pl' }, url = 'http://example.com/email' )
  33. 33. Patch - method instead of return_value @patch.object(requests, 'post', autospec=True) def test_should_send_message_tooo(self, post_mock): # GIVEN def implementation_from_unit_test(*args, **kwargs): return self._get_success_response() post_mock.side_effect = implementation_from_unit_test ...
  34. 34. Patch - nesting patches @patch.object(requests, 'post', autospec=True) @patch.object(requests, 'get', autospec=True) @patch.object(requests, 'put', autospec=True) def test_should_send_message_tooo(self, put_mock, get_mock, post_mock): # GIVEN ... # WHEN ... # THEN ...
  35. 35. Patch - TestCase class MessageSenderTests(unittest.TestCase): @patch.object(requests, 'post', autospec=True) def test_should_send_message(self, post_mock): pass @patch.object(requests, 'post', autospec=True) def test_should_not_catch_exception(self, post_mock): pass @patch.object(requests, 'post', autospec=True) class MessageSenderTests(unittest.TestCase): def test_should_send_message(self, post_mock): pass def test_should_not_catch_exception(self, post_mock): pass
  36. 36. Patch - in setUp class MessageSenderTests(unittest.TestCase): def setUp(self): super().setUp() patcher = patch('requests.post') self.addCleanup(patcher.stop) self._post_mock = patcher.start() def test_should_send_message(self): # GIVEN self._post_mock.return_value = self._get_success_response() # WHEN ... # THEN ...
  37. 37. Patch - environment variables class MessageSenderTests(unittest.TestCase): @patch.dict('os.environ', {'not_existing_key': 'some_value'}, clear=True) def test_should_override_environment_variables(self): self.assertEqual(os.environ, {'not_existing_key': 'some_value'})
  38. 38. Summary Use mock when you can pass it Use patch when you can not pass mock
  39. 39. Thank you! Questions?

×