Python Mock and Autospec

Blog

by Maria on 23 Aug 2018 - 05:16  

Auto speccing means that your fake object will have the same signature as the object it is mocking. This can come in very handy if you want your fake object to receive the same input that your original object would.

First let's look at an example where the inputs don't matter. Below I have made an abbreviated fake request response object for mocking requests.post. Note that this looks very different from the actual response object returned from the requests library.

Actual Response object: http://docs.python-requests.org/en/master/_modules/requests/models/#Response

In particular the json method signature is different, and it is missing all manner of methods and attributes. And, more importantly we weren't even trying to mock post itself, iow we don't care at all what parameters were used to call requests.post, we are only faking what it is returning.

But, it returns the stuff that our code expects and uses.

Here is the mock:

class FakeRequest():
    """

    Fake a response from the request library

    """


    status_code = 201

    original = {"result": {"silliness added"}}

    text = json.dumps(original)

    content = bytes(text, 'utf-8')

    def json(self):

        """

        Return a small subset of what the api might return

        """


        return self.original
 

And here is an example of using that mock in a test:


@patch.object(requests, 'post')

    def test_can_post(self, mock_requests):

        """

        Verify call actually happens and data is returned on single step post

        Mocking this, to limit hitting an actual endpoint.

        """


        mock_requests.return_value = FakeRequest()

        processed_stuff = run_code_using_request()

        self.assertEqual(processed_stuff, "I made a thing")
 

In this case, we did not care what the inputs to the requests post method was or anything beyond what was in our fake request. So, we could create a fake object that looked nothing like what we were mocking. This can be very handy, and it worked fine, because we were not trying to call our fake post with any parameters, and especially because we were not trying to call it with any parameters that are internal to the place where we are mocking. However, if we want to mock something that is being called internally with parameters that are specific to the internal code, and we care about those parameters, life becomes more difficult.

I ran into this when trying to mock middleware for Flask. Here is what the middleware class you create looks like when you add middleware to flash:

class SillyMiddleWare(object):

“””Silly WSGI middleware

“””

def __init__(self, app):

    self.app = app

def __call__(self, environ, start_response):
    """
    Stuff that happens when an endpoint is hit, but before the endpoint code runs
    """


    print(‘doing something silly in every http request’)

    return self.app(environ, start_response)
 

Now, I wanted to bypass the middleware, because it was adding an authentication layer I didn't want to use for most of my tests, but clearly I couldn't do the same thing I had done to mock the class returned by requests. I needed my mock to return its input parameters, because that is what the __call__ method is doing. Turns out this is easy with autospec. Since I needed to do this almost everywhere, I put it in my setup for my basetest, which is subclassed by all of my tests. This way, for the the tests that did not need it, I could just over-ride it.

Because I was mocking a class method, I needed to include an input for self:


def mock_acls(self_acls, environ, start_response):

    return self_acls.app(environ, start_response)


class TestStuff(unittest.TestCase)

    def setUp(self):

        self.acls_mock_patch = mock.patch.object(ACLMiddleware, "__call__", autospec=True,
                                                                                side_effect=mock_acls)

        self.acls_mock = self.acls_mock_patch.start()


    def tearDown(self):

        self.acls_mock_patch.stop()
 

Now, this would not have worked at all with our requests example, because autospec forces us to have objects that have the same signature (inputs and outputs) as the original object. This has more implications if you are patching an entire class. With autospec you cannot add methods or attributes that are not in the class you are mocking. This can be a good thing, as it is possible for tests to pass when using mock, even if the code is broken.

See this article: Mocking has a Weakness, Speccing Removes It

Note from the above example, that it is possible to mock just one method from a class, you just have to make sure you include a self variable.

Share


Comments: 0

Contact me if you want to comment (I won't publish your email address):

Subject: Subject:

Name:
Email:
Comments:

Enter code:

  LinkedIn