Tags: #python #mocking #mocks #tornado
open
functionThe mock
module is a backwards compatible library you can download from PyPy, where as unittest.mock
is the same thing but only compatible with the version of Python you’re using.
So in almost all cases you’ll want to import it like so:
import unittest.mock as mock
For more examples, see this reference guide
In the following example our code (in app.account.confirm(...)
) catches a generic Exception
and re-raises it as exceptions.CognitoException
, which we catch and make assertions against:
@mock.patch('app.aws.sdk.confirm_sign_up', side_effect=Exception('whoops'))
@mock.patch('app.account.instr_exc')
def test_account_confirm_failure(mock_instr_exc, mock_signup):
with pytest.raises(exceptions.CognitoException) as exc_info:
app.account.confirm(123, 'foo')
assert True is True # this will never be executed!
assert exc_info.typename == 'CognitoException'
assert str(exc_info.value) == 'SIGNUP_CONFIRMATION_FAILED'
# we can't have a single assert against call_args_list because in python
# two instances of an exception aren't considered equal, and so we have to
# assert individual elements of call_args_list.
assert type(mock_instr_exc.call_args_list[0][0][0]) == Exception
assert str(mock_instr_exc.call_args_list[0][0][0]) == 'whoops'
assert mock_instr_exc.call_args_list[0][0][1] == 'SIGNUP_CONFIRMATION_FAILED'
assert mock_instr_exc.call_args_list[0][1] == {'code': '123'}
Note: don’t make the mistake of putting any assertions within the
with
context manager. Once the Exception is raised by the function being called within thewith
context manager, all code after it inside the block is not executed.
If a function you wish to test has the functools.lru_cache
decorator applied, then you’ll need to be mindful of mocking the response of that function as it’ll be cached in one test and the cached result will be returned when calling the function again to test some other behaviour (and might likely confuse you when you see the unexpected response).
To fix this issue is very easy because lru_cache
provides additional functions when decoratoring your functions, it provides:
cache_info
cache_clear
The latter (cache_clear
) is what you call…
@lru_cache(5)
def foo():
print('Executing foo...')
foo() # Executing foo...
foo() # <nothing printed as None response was cached and returned>
foo.cache_info() # CacheInfo(hits=1, misses=1, maxsize=5, currsize=1)
foo.cache_clear()
foo() # Executing foo... (notice the 'side effect of print is executed again)
Note: debugging this isn’t always obvious. Later on I demonstrate how to mock the builtin
open
function, and in that scenario I stumbled across this issue, because although I wasn’t mocking the top level function itself (I was mocking the call toopen
within), the contents of the file being opened was what was returned and being cached.
With a module variable you can can either set the value directly or use mock.patch
.
In the following example we have the variable client_id
which is a global variable inside the app.aws
module which we import to reference elsewhere in our code:
import app.aws
def test_account_confirm_successful():
app.aws.client_id = 456 # used internally by `confirm()`
...
@mock.patch('app.aws.client_id', 456)
def test_account_confirm_successful():
...
In the mock.patch
example, there are two key things to notice:
return_value
.Mock the entire class and take advantage of the fact that a mock, when called, returns a new mock:
@mock.patch("foo.bar.SomeClass")
def test_stuff(mock_class):
mock_class.return_value.made_up_function.return_value = "123"
The reason this ^^ works is because calling a mock returns another mock, and so if you call mock_class.return_value
you’re actually getting another mock object; and because you can call anything you like on a mock object (a function or property you call on a mock doesn’t have to exist), means you can set a return_value
on the mock that’s returned by calling made_up_function
.
Similar approach to mocking an instance method in that you mock the entire class but you have one less return_value
to assign to:
mock_class.ClassMethodName.return_value = "123"
To mock an entire class you’ll need to set the return_value
to be a new instance of the class.
@mock.patch('myapp.app.Car')
def test_class(self, mock_car):
class NewCar(object):
def get_make(self):
return 'Audi'
@property
def wheels(self):
return 6
mock_car.return_value = NewCar()
...
See other class related mocking tips here
We can create a coroutine and allow it to be configurable for different types of responses:
def make_coroutine(response):
"""You could pass response as a mock or as a raw data structure."""
async def coroutine(*args, **kwargs):
"""Imagine this coroutine is called with a url as the first argument."""
if args[0] == '/exception/foo':
raise Exception('Whoops Foo')
elif args[0] == '/exception/bar':
raise Exception('Whoops Bar')
return response
return coroutine
class TestTornado(AsyncHTTPSTestCase):
def get_app(self):
class FakeHandler(tornado.web.RequestHandler):
async def get(self, *args, **kwargs):
self.finish('hello')
return tornado.web.Application([
(r'/fake', FakeHandler),
])
@mock.patch('foo.bar.http_client')
def test_async thing(self, mock_client):
response_body = {'state': 'success', 'payload': 'foobar'}
response_mock = mock.MagicMock()
response_mock.body = json.dumps(response_body)
mock_client.post.side_effect = make_coroutine(response_mock)
response = self.fetch('/fake')
assert response.code == 200
assert json.loads(response.body.decode()) == response_body
Note: you could also create a
MagicMock
and set properties on it likem = mock.MagicMock(x=1, y=2, z=3)
and then pass that into themake_coroutine
function. That way, within eachif
statement you can then just callm.x
orm.y
etc to get at the actual response you want to return (rather than having to hardcode response objects within the function itself). Mock is also consideredcallable
(see below implementation).
When dealing with side_effects that need to sometimes trigger an Exception and other times suceed you could use a slightly modified mock implementation that checks if the given response object is callable or not…
count = 0
def make_side_effect_coroutine(side_effect):
"""Side effect friendly mock coroutine.
In some tests we need to have a mocked coroutine return a different value
when it's called multiple times, but a mock side_effect can't trigger a
raised exception when given an iterator, and so we have to construct that
behaviour ourselves.
"""
async def coroutine(*args, **kwargs):
return side_effect(*args, **kwargs) if callable(side_effect) else side_effect
return coroutine
@mock.patch('app.thing')
def test_confirm_email_change_failure(self, mock_thing):
def side_effects(*args, **kwargs):
"""Use global var to control mock side effects."""
global count
if count > 0:
raise Exception('whoops')
count += 1
return # don't raise an exception the first time around
mock_thing.side_effect = make_side_effect_coroutine(side_effects)
Alternatives…
# allow mock to be used as an await expression...
async def async_response():
return namedtuple('_', ['body'])('{"state": "success"}')
def mock_async_expression(our_mock):
return async_response().__await__()
mock.MagicMock.__await__ = mock_async_expression
class AsyncMock(MagicMock):
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
class TestHandlers(testing.AsyncTestCase):
@mock.patch('app.handlers.trigger_soft_cdn_purge', new_callable=AsyncMock)
@mock.patch('app.handlers.api')
@testing.gen_test
async def test_update_cache(self, api_mock, trigger_soft_cdn_purge):
response = mock.MagicMock()
response.code = 200
api_mock.buzz = AsyncMock(return_value=response)
@mock.patch('app.buzz_api.api_gateway')
@testing.gen_test
async def test_buzz_api(self, client_mock):
async def get(url, **kwargs):
return
client_mock.get.side_effect = get
There are two ways to make a mock more like the real object being mocked.
spec
wrap
We can use mock’s spec
feature to mimic all methods/attributes of the object being mocked. This ensures your mocks have the same api as the objects they are replacing.
Note: there is a stricter
spec_set
that will raise anAttributeError
.
This is best demonstrated with an example:
import unittest.mock as mock
import tornado.simple_httpclient
from tornado.httpclient import AsyncHTTPClient
http_client = AsyncHTTPClient()
type(http_client) # tornado.simple_httpclient.SimpleAsyncHTTPClient
isinstance(http_client, tornado.simple_httpclient.SimpleAsyncHTTPClient) # True
isinstance(mock.MagicMock(), tornado.simple_httpclient.SimpleAsyncHTTPClient) # False
m = mock.MagicMock(spec=tornado.simple_httpclient.SimpleAsyncHTTPClient)
isinstance(m, tornado.simple_httpclient.SimpleAsyncHTTPClient) # True
The wrap
parameter allows you to ‘spy’ on the implementation, as wll as affect it’s behaviour:
@pytest.mark.parametrize("input_date, input_url, valid", [
("2017-06-17T00:00:00.000000Z", "foo", True),
("2017-06-18T00:00:00.000000Z", "bar", False),
])
@mock.patch("app.handlers.data.datetime", wraps=datetime)
def test_valid_video(mock_datetime, input_date, input_url, valid):
mock_datetime.now.return_value = datetime(2017, 6, 18, 00, 00, 00, 000000)
assert valid_video(input_date, input_url) is valid
If your function has a try/except around it, then you can use side_effect
to cause the calling of the function to trigger an Exception as the returned value:
@mock.patch('app.aws.sdk.confirm_sign_up', side_effect=Exception('whoops'))
Note: if you had used
return_value=Exception('whoops')
then the mock would return the string representation of the Exception rather than raising an exception likeside_effect
does.
Otherwise if you just need a static value returned, so it’s evaluated at the time it’s defined (not when it’s called), then you can use return_value
instead:
@mock.patch('app.security.secret_hash', return_value='###')
Calling a property on a mock returns another mock, so in order to mock very specific properties you’ll need to nest your return_value
or side_effect
:
m = mock.MagicMock()
m.return_value.get.side_effect = [1, 2]
m.return_value.post.return_value = 'foo'
x = m()
x.get() # 1
x.post() # foo
x.get() # 2
open
functionPython’s mock library provides an abstraction for mocking the builtin open
function a lot simpler…
def test_load_ui_messages_successful():
"""Verify ui message YAML file can be read properly."""
file_content = 'foo: bar'
with mock.patch('bf_auth.utility.open', mock.mock_open(read_data=file_content), create=True) as mock_builtin_open:
assert utils.load_ui_messages('./path/to/non/existing/file.yaml') == {'foo': 'bar'}
The create=True
param set on mock.patch
means that the mock.MagicMock
returned will automagically create any attributes that are called on the mock (this is because the open
function will attempt to access lots of different things and it’s easier for mock to mock out all of that for you).