erip erip - 2 months ago 8
Python Question

How can I use unittest.mock to remove side effects from code?

I have a function with several points of failure:

def setup_foo(creds):
"""
Creates a foo instance with which we can leverage the Foo virtualization
platform.

:param creds: A dictionary containing the authorization url, username,
password, and version associated with the Foo
cluster.
:type creds: dict
"""

try:
foo = Foo(version=creds['VERSION'],
username=creds['USERNAME'],
password=creds['PASSWORD'],
auth_url=creds['AUTH_URL'])

foo.authenticate()
return foo
except (OSError, NotFound, ClientException) as e:
raise UnreachableEndpoint("Couldn't find auth_url {0}".format(creds['AUTH_URL']))
except Unauthorized as e:
raise Unauthorized("Wrong username or password.")
except UnsupportedVersion as e:
raise Unsupported("We only support Foo API with major version 2")


and I'd like to test that all the relevant exceptions are caught (albeit not handled well currently).

I have an initial test case that passes:

def test_setup_foo_failing_auth_url_endpoint_does_not_exist(self):
dummy_creds = {
'AUTH_URL' : 'http://bogus.example.com/v2.0',
'USERNAME' : '', #intentionally blank.
'PASSWORD' : '', #intentionally blank.
'VERSION' : 2
}
with self.assertRaises(UnreachableEndpoint):
foo = osu.setup_foo(dummy_creds)


but how can I make my test framework believe that the AUTH_URL is actually a valid/reachable URL?

I've created a mock class for
Foo
:

class MockFoo(Foo):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


and my thought is mock the call to
setup_foo
and remove the side effect of raising an
UnreachableEndpoint
exception. I know how to add side-effects to a
Mock
with
unittest.mock
, but how can I remove them?

Answer

Assuming your exceptions are being raised from foo.authenticate(), what you want to realize here is that it does not necessarily matter whether the data is in fact really valid in your tests. What you are trying to say really is this:

When this external method raises with something, my code should behave accordingly based on that something.

So, with that in mind, what you want to do is have different test methods where you pass what should be valid data, and have your code react accordingly. The data itself does not matter, but it provides a documented way of showing how the code should behave with data that is passed in that way.

Ultimately, you should not care how the nova client handles the data you give it (nova client is tested, and you should not care about it). What you care about is what it spits back at you and how you want to handle it, regardless of what you gave it.

In other words, for the sake of your tests, you can actually pass a dummy url as:

"this_is_a_dummy_url_that_works"

For the sake of your tests, you can let that pass, because in your mock, you will raise accordingly.

For example. You mock out the authenticate method to raise:

@patch('module_to_code.authenticate')
def test_setup_foo_failing_auth_url_endpoint_does_not_exist(self, authenticate_mock):
    authenticate_mock.side_effect = Unauthorized

    dummy_creds = {
        'AUTH_URL' : 'i_dont_care_what_this_is_but_its_valid_in_this_test',
        'USERNAME' : 'bad_user_but_who_cares_what_it_is',
        'PASSWORD' : 'bad_pass_but_it_does_not_matter_what_this_is',
        'VERSION'  : 22237582735 
    }
    with self.assertRaises(Unauthorized):
        foo = osu.setup_foo(dummy_creds)

So, the main idea with the example above, is that it does not matter what data you are passing. What really matters is that you are wanting to know how your could will react when an external method raises.

To address your question in the comment, the argument authenticate_mock is actually going to be given by the @patch decorator that is mocking authenticate. So, you should not have to do anything else beyond that. authenticate, should be mocked now, and in your test method you can manipulate accordingly.

To help, here is a simple example that illustrates this:

some_module.py

class CustomException(Exception):
    pass


def some_function():
    return True


def my_function():
    try:
        d = some_function()
    except AttributeError as e:
        raise CustomException(e)
    return d

test_this.py

import unittest
from unittest.mock import patch

from some_module import my_function, CustomException


class TestCompare(unittest.TestCase):

    @patch("some_module.some_function")
    def test_compare(self, mock_some_function):
        mock_some_function.side_effect = AttributeError

        with self.assertRaises(CustomException):
            self.assertEqual(my_function(), "1 1")