Share
Embed
Adventures with Mock
Slides for a talk on using python & mock. Given at PyOhio 2013
Brad Montgomery
July 27, 2013
Transcript
83 slides
-
Getting Started In the Standard Library as of Python 3.3+
>>> from unittest import mock Sunday, July 28, 13 -
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 -
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 -
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 -
Unit Testing • Exercise part of your code: • function
or method • Verify the side-effects • May Verify State Change Sunday, July 28, 13 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
# main.py import requests requests library <Mock> Where to patch?
Sunday, July 28, 13 -
# 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 -
# 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 -
# 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 -
# 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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
Customizing Mock Objects • Set Attribute Values • Set Method
Return Values • Create chains of Method calls Sunday, July 28, 13 -
Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',
'email': '[email protected]', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13 -
Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',
'email': '[email protected]', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13 -
Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',
'email': '[email protected]', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13 -
Customizing Mock Objects config = { 'first_name': 'Alan', 'last_name': 'Turing',
'email': '[email protected]', 'get_achievement.return_value': 'Computing', 'get_age.side_effect': ValueError } m = Mock(**config) Sunday, July 28, 13 -
Customizing Mock Objects >>> m.first_name 'Alan' >>> m.get_achievement() 'Computing' >>>
m.get_age() ValueError Sunday, July 28, 13 -
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 -
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 -
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 -
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 -
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 -
Tips The Documentation is Awesome http://www.voidspace.org.uk/python/mock/ tons of examples for
various scenarios! Sunday, July 28, 13