Adventures with Mock

Adventures with Mock

Slides for a talk on using python & mock. Given at PyOhio 2013

D57aec10399cbb252bd890c2bb3fe1c9?s=128

Brad Montgomery

July 27, 2013
Tweet

Transcript

  1. Sunday, July 28, 13

  2. About Me Brad Montgomery Twitter: @bkmontgomery https://github.com/bradmontgomery https://workforpie.com/bkmontgomery Sunday, July

    28, 13
  3. Disclaimer Sunday, July 28, 13

  4. Getting Started In the Standard Library as of Python 3.3+

    >>> from unittest import mock Sunday, July 28, 13
  5. Getting Started For Python 2.x pip install mock Sunday, July

    28, 13
  6. What is Mock? “mock allows you to replace parts of

    your system under test with mock objects and make assertions about how they have been used.” Sunday, July 28, 13
  7. What is Mock? “mock allows you to replace parts of

    your system under test with mock objects and make assertions about how they have been used.” Sunday, July 28, 13
  8. What is Mock? “mock allows you to replace parts of

    your system under test with mock objects and make assertions about how they have been used.” Sunday, July 28, 13
  9. Typical Initial Experience with Mock Sunday, July 28, 13

  10. Unit Testing • Exercise part of your code: • function

    or method • Verify the side-effects • May Verify State Change Sunday, July 28, 13
  11. Typical Unit Test import unittest def add(x, y): return x

    + y class TestAdd(unittest.TestCase): def test_add(self): result = add(40, 2) self.assertEqual(result, 42) Sunday, July 28, 13
  12. import unittest def add(x, y): return x + y class

    TestAdd(unittest.TestCase): def test_add(self): result = add(40, 2) self.assertEqual(result, 42) Typical Unit Test Sunday, July 28, 13
  13. import unittest def add(x, y): return x + y class

    TestAdd(unittest.TestCase): def test_add(self): result = add(40, 2) self.assertEqual(result, 42) Typical Unit Test Sunday, July 28, 13
  14. def add_service(x, y): resp = requests.post( "http://math.biz/sum", {'operands': [x, y]}

    ) return resp.json() class TestAdd(unittest.TestCase): def test_add_service(self): result = add_service(40, 2) self.assertEqual(result, 42) What if... Sunday, July 28, 13
  15. def add_service(x, y): resp = requests.post( "http://math.biz/sum", {'operands': [x, y]}

    ) return resp.json() class TestAdd(unittest.TestCase): def test_add_service(self): result = add_service(40, 2) self.assertEqual(result, 42) What if... Sunday, July 28, 13
  16. def add_service(x, y): resp = requests.post( "http://math.biz/sum", {'operands': [x, y]}

    ) return resp.json() class TestAdd(unittest.TestCase): def test_add_service(self): result = add_service(40, 2) self.assertEqual(result, 42) What if... Dependency! Sunday, July 28, 13
  17. def add_service(x, y): resp = requests.post( "http://math.biz/sum", {'operands': [x, y]}

    ) return resp.json() class TestAdd(unittest.TestCase): def test_add_service(self): result = add_service(40, 2) self.assertEqual(result, 42) What if... Fragile! Sunday, July 28, 13
  18. Use Mock • Replace part of your code with something

    you can inspect! • Replace requests with a mock object • Assert that requests.post was called with correct parameters Sunday, July 28, 13
  19. Mock Objects • Flexible objects that replace other parts of

    your code. • Callable. • Creates Attributes when accessed (new Mock objects) • Record how they’re used (& you can make assertions about that!) Sunday, July 28, 13
  20. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) Sunday, July 28, 13
  21. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) Sunday, July 28, 13
  22. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) Different Mock objects Sunday, July 28, 13
  23. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) Sunday, July 28, 13
  24. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) *Still* Different Mock objects! Sunday, July 28, 13
  25. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) Sunday, July 28, 13
  26. Mock Objects >>> from mock import Mock # Python 2

    >>> from unittest.mock import Mock # Python 3 >>> # Create a Mock object >>> m = Mock() >>> m <Mock id='4311593936'> # Access arbitrary attributes >>> m.some_value <Mock name='mock.some_value' id='4311595472'> # Call arbitrary methods with arbitrary parameters >>> m.get_result(value=42) <Mock name='mock.get_result()' id='4311568144'> # Make assertions about how it was called >>> m.get_result.assert_called_once_with(value=42) AssertionError if this is wrong. Sunday, July 28, 13
  27. from mock import patch • Temporarily replaces a named object

    with a Mock object. • e.g. Replaces requests with an instance of Mock • Confusing. Sunday, July 28, 13
  28. Where to patch? # main.py import requests def add_service(x, y):

    resp = requests.get( "http://math.biz/sum", {'operands': [x, y]} ) return resp.json() # tests.py import unittest from mock import patch from main import add_service class TestAdd(unittest.TestCase): def test_add(self): # ... Sunday, July 28, 13
  29. Where to patch? # main.py import requests def add_service(x, y):

    resp = requests.get( "http://math.biz/sum", {'operands': [x, y]} ) return resp.json() # tests.py import unittest from mock import patch from main import add_service class TestAdd(unittest.TestCase): def test_add(self): # ... Replace requests Sunday, July 28, 13
  30. Where to patch? # main.py import requests def add_service(x, y):

    resp = requests.get( "http://math.biz/sum", {'operands': [x, y]} ) return resp.json() # tests.py import unittest from mock import patch from main import add_service class TestAdd(unittest.TestCase): def test_add(self): # ... Replace requests, which lives in main.py Sunday, July 28, 13
  31. patch where your code looks for an object. Sunday, July

    28, 13
  32. # main.py import requests requests library <Mock> Where to patch?

    Sunday, July 28, 13
  33. # main.py import requests requests library <Mock> Where to patch?

    Patch requests in main.py so it references a mock object. Sunday, July 28, 13
  34. # tests.py import unittest from mock import patch from main

    import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Let’s Test! Sunday, July 28, 13
  35. # tests.py import unittest from mock import patch from main

    import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Let’s Test! Sunday, July 28, 13
  36. # tests.py import unittest from mock import patch from main

    import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Let’s Test! requests is referenced from the main module Sunday, July 28, 13
  37. Let’s Test! # tests.py import unittest from mock import patch

    from main import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Sunday, July 28, 13
  38. Let’s Test! # tests.py import unittest from mock import patch

    from main import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Sunday, July 28, 13
  39. Let’s Test! # tests.py import unittest from mock import patch

    from main import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Sunday, July 28, 13
  40. Let’s Test! # tests.py import unittest from mock import patch

    from main import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Sunday, July 28, 13
  41. Let’s Test! # tests.py import unittest from mock import patch

    from main import add_service class TestAdd(unittest.TestCase): def test_add_service(self): with patch("main.requests") as mock_requests: add_service(40, 2) # Call our function # Verify .post() was called as expected mock_requests.post.assert_called_once_with( "http://math.biz/sum", {'operands': [40, 2]} ) Sunday, July 28, 13
  42. Other Mocking Opportunities • Code that hits External APIS/Network Resources

    • Exception Handler Code • “Expensive” code • Code that needs a lot of setup Sunday, July 28, 13
  43. Testing Exception Handlers # main.py import json import logging import

    requests from requests import ConnectionError, HTTPError def get_content(url): try: response = requests.get(url) result = json.loads(response.json()) except (ConnectionError, HTTPError): logging.warn("No Result") result = {} return result Sunday, July 28, 13
  44. Testing Exception Handlers # main.py import json import logging import

    requests from requests import ConnectionError, HTTPError def get_content(url): try: response = requests.get(url) result = json.loads(response.json()) except (ConnectionError, HTTPError): logging.warn("No Result") result = {} return result Sunday, July 28, 13
  45. Testing Exception Handlers # main.py import json import logging import

    requests from requests import ConnectionError, HTTPError def get_content(url): try: response = requests.get(url) result = json.loads(response.json()) except (ConnectionError, HTTPError): logging.warn("No Result") result = {} return result How do you test this? Sunday, July 28, 13
  46. Testing Exception Handlers # tests.py from main import add_service, get_content

    from mock import patch import requests import unittest class TestAdd(unittest.TestCase): # ... def test_get_content_exception_handler(self): with patch("main.requests") as mock_requests: url = "http://example.com" mock_requests.get.side_effect = requests.HTTPError result = get_content(url) mock_requests.get.assert_called_once_with(url) self.assertEqual(result, {}) Sunday, July 28, 13
  47. Testing Exception Handlers # tests.py from main import add_service, get_content

    from mock import patch import requests import unittest class TestAdd(unittest.TestCase): # ... def test_get_content_exception_handler(self): with patch("main.requests") as mock_requests: url = "http://example.com" mock_requests.get.side_effect = requests.HTTPError result = get_content(url) mock_requests.get.assert_called_once_with(url) self.assertEqual(result, {}) Sunday, July 28, 13
  48. Testing Exception Handlers # tests.py from main import add_service, get_content

    from mock import patch import requests import unittest class TestAdd(unittest.TestCase): # ... def test_get_content_exception_handler(self): with patch("main.requests") as mock_requests: url = "http://example.com" mock_requests.get.side_effect = requests.HTTPError result = get_content(url) mock_requests.get.assert_called_once_with(url) self.assertEqual(result, {}) Sunday, July 28, 13
  49. Testing Exception Handlers # tests.py from main import add_service, get_content

    from mock import patch import requests import unittest class TestAdd(unittest.TestCase): # ... def test_get_content_exception_handler(self): with patch("main.requests") as mock_requests: url = "http://example.com" mock_requests.get.side_effect = requests.HTTPError result = get_content(url) mock_requests.get.assert_called_once_with(url) self.assertEqual(result, {}) Sunday, July 28, 13
  50. Testing Exception Handlers # tests.py from main import add_service, get_content

    from mock import patch import requests import unittest class TestAdd(unittest.TestCase): # ... def test_get_content_exception_handler(self): with patch("main.requests") as mock_requests: url = "http://example.com" mock_requests.get.side_effect = requests.HTTPError result = get_content(url) mock_requests.get.assert_called_once_with(url) self.assertEqual(result, {}) Sunday, July 28, 13
  51. Testing Exception Handlers # tests.py from main import add_service, get_content

    from mock import patch import requests import unittest class TestAdd(unittest.TestCase): # ... def test_get_content_exception_handler(self): with patch("main.requests") as mock_requests: url = "http://example.com" mock_requests.get.side_effect = requests.HTTPError result = get_content(url) mock_requests.get.assert_called_once_with(url) self.assertEqual(result, {}) Sunday, July 28, 13
  52. SIDE NOTE! def get_content(url): try: response = requests.get(url) result =

    json.loads(response.json()) except (ConnectionError, HTTPError): logging.warn("No Result") result = {} return result $ coverage run tests.py && coverage report Sunday, July 28, 13
  53. SIDE NOTE! def get_content(url): try: response = requests.get(url) result =

    json.loads(response.json()) except (ConnectionError, HTTPError): logging.warn("No Result") result = {} return result $ coverage run tests.py && coverage report coverage helps you know what code is covered by tests! Sunday, July 28, 13
  54. Mock’s side_effect • Controls the side effects of calling a

    method • Can raise Exceptions • Can also produce dynamic results for function/method calls* *skipping examples because time! Sunday, July 28, 13
  55. Other Mocking Opportunities • Code that hits External APIS/Network Resources

    • Exception Handler Code • “Expensive” code • Code that needs a lot of setup Sunday, July 28, 13
  56. Mocking Expensive Code # main.py class SciWhizBang(object): def _calc(self, value):

    import time time.sleep(1000) # Expensive! return value + 1 def calc_alpha(self): return self._calc(100) def calc_beta(self): return self._calc(999) Sunday, July 28, 13
  57. Mocking Expensive Code # main.py class SciWhizBang(object): def _calc(self, value):

    import time time.sleep(1000) # Expensive! return value + 1 def calc_alpha(self): return self._calc(100) def calc_beta(self): return self._calc(999) Sunday, July 28, 13
  58. Mocking Expensive Code # main.py class SciWhizBang(object): def _calc(self, value):

    import time time.sleep(1000) # Expensive! return value + 1 def calc_alpha(self): return self._calc(100) def calc_beta(self): return self._calc(999) How do I test this? Sunday, July 28, 13
  59. Mocking Expensive Code # main.py class SciWhizBang(object): def _calc(self, value):

    import time time.sleep(1000) # Expensive! return value + 1 def calc_alpha(self): return self._calc(100) def calc_beta(self): return self._calc(999) How do I test this? without calling this? Sunday, July 28, 13
  60. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  61. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  62. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  63. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  64. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  65. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  66. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  67. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  68. Mocking Expensive Code # tests.py class TestSciWhizBang(unittest.TestCase): @patch.object(SciWhizBang, "_calc", return_value=42)

    def test_calc_alpha(self, mock_calc): obj = SciWhizBang() self.assertEqual(obj.calc_alpha(), 42) mock_calc.assert_called_once_with(100) Sunday, July 28, 13
  69. Other Mocking Opportunities • Code that hits External APIS/Network Resources

    • Exception Handler Code • “Expensive” code • Code that needs a lot of setup Sunday, July 28, 13
  70. Customizing Mock Objects • Set Attribute Values • Set Method

    Return Values • Create chains of Method calls Sunday, July 28, 13
  71. Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',

    'email': 'turing@example.com', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13
  72. Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',

    'email': 'turing@example.com', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13
  73. Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',

    'email': 'turing@example.com', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13
  74. Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',

    'email': 'turing@example.com', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13
  75. Customizing Mock Objects >>> m.first_name 'Alan' >>> m.get_achievement() 'Computing' >>>

    m.get_age() ValueError Sunday, July 28, 13
  76. Spec’d objects • Creates a “white list” of available attributes/

    method names. • Anything else raises an Attribute Error user_spec = [ 'first_name', 'last_name', 'email', 'get_full_name' ] m = Mock(spec=user_spec) Sunday, July 28, 13
  77. Spec’d objects user_spec = [ 'first_name', 'last_name', 'email', 'get_full_name' ]

    m = Mock(spec=user_spec) >>> m.get_full_name() <Mock name='mock.get_full_name()' id='4311640208'> >>> m.password AttributeError Sunday, July 28, 13
  78. Spec’d objects user_spec = [ 'first_name', 'last_name', 'email', 'get_full_name' ]

    m = Mock(spec=user_spec) >>> m.get_full_name() <Mock name='mock.get_full_name()' id='4311640208'> >>> m.password AttributeError Sunday, July 28, 13
  79. Autospec • Automatically creates a spec from an existing object.

    • Anything else raises an Attribute Error from mock import create_autospec >>> u = User.objects.get(pk=1) >>> m = create_autospec(u) >>> m.get_full_name() <MagicMock name='mock.get_full_name()' id='4504021136'> >>> m.something_else AttributeError Sunday, July 28, 13
  80. Mock • Create Mock objects instead of loading fixtures •

    Narrowly define your Mock object’s behavior / Expect Assertions if they’re used incorrectly. • Rely on Spec/Autospec’d objects as test objects Sunday, July 28, 13
  81. Tips The Documentation is Awesome http://www.voidspace.org.uk/python/mock/ tons of examples for

    various scenarios! Sunday, July 28, 13
  82. Tips Sunday, July 28, 13

  83. Questions? & Thank You! Sunday, July 28, 13