175

I have a function (foo) which calls another function (bar). If invoking bar() raises an HttpError, I want to handle it specially if the status code is 404, otherwise re-raise.

I am trying to write some unit tests around this foo function, mocking out the call to bar(). Unfortunately, I am unable to get the mocked call to bar() to raise an Exception which is caught by my except block.

Here is my code which illustrates my problem:

import unittest
import mock
from apiclient.errors import HttpError


class FooTests(unittest.TestCase):
    @mock.patch('my_tests.bar')
    def test_foo_shouldReturnResultOfBar_whenBarSucceeds(self, barMock):
        barMock.return_value = True
        result = foo()
        self.assertTrue(result)  # passes

    @mock.patch('my_tests.bar')
    def test_foo_shouldReturnNone_whenBarRaiseHttpError404(self, barMock):
        barMock.side_effect = HttpError(mock.Mock(return_value={'status': 404}), 'not found')
        result = foo()
        self.assertIsNone(result)  # fails, test raises HttpError

    @mock.patch('my_tests.bar')
    def test_foo_shouldRaiseHttpError_whenBarRaiseHttpErrorNot404(self, barMock):
        barMock.side_effect = HttpError(mock.Mock(return_value={'status': 500}), 'error')
        with self.assertRaises(HttpError):  # passes
            foo()

def foo():
    try:
        result = bar()
        return result
    except HttpError as error:
        if error.resp.status == 404:
            print '404 - %s' % error.message
            return None
        raise

def bar():
    raise NotImplementedError()

I followed the Mock docs which say that you should set the side_effect of a Mock instance to an Exception class to have the mocked function raise the error.

I also looked at some other related StackOverflow Q&As, and it looks like I am doing the same thing they are doing to cause and Exception to be raised by their mock.

Why is setting the side_effect of barMock not causing the expected Exception to be raised? If I am doing something weird, how should I go about testing logic in my except block?

Community
  • 1
  • 1
Jesse Webb
  • 43,135
  • 27
  • 106
  • 143
  • I'm pretty sure your exception *is* being raised, but I'm not sure how you are setting the `resp.status` code there. Where does `HTTPError` come from? – Martijn Pieters Feb 03 '15 at 17:48
  • @MartijnPieters `HttpError` is a class defined in [Google's `apiclient` lib](https://developers.google.com/api-client-library/python/) which we use in GAE. It's `__init__` is defined with the params `(resp, content)` so I was attempting to create a mock instance for the response, with the appropriate status code specified. – Jesse Webb Feb 03 '15 at 17:57
  • Right, so it is [this class](https://github.com/google/google-api-python-client/blob/master/googleapiclient/errors.py#L35-L63); but you don't need to user `return_value` then; `resp` is not being *called*. – Martijn Pieters Feb 03 '15 at 18:01
  • 1
    I tried my code again without using `HttpError` and instead tried using just a regular `Exception` instance. This works perfectly. This means that it must have something to do with how I am configuring the `HttpError` instance, probably related to how I am creating a `Mock` instance for the response. – Jesse Webb Feb 03 '15 at 18:05

1 Answers1

205

Your mock is raising the exception just fine, but the error.resp.status value is missing. Rather than use return_value, just tell Mock that status is an attribute:

barMock.side_effect = HttpError(mock.Mock(status=404), 'not found')

Additional keyword arguments to Mock() are set as attributes on the resulting object.

I put your foo and bar definitions in a my_tests module, added in the HttpError class so I could use it too, and your test then can be ran to success:

>>> from my_tests import foo, HttpError
>>> import mock
>>> with mock.patch('my_tests.bar') as barMock:
...     barMock.side_effect = HttpError(mock.Mock(status=404), 'not found')
...     result = my_test.foo()
... 
404 - 
>>> result is None
True

You can even see the print '404 - %s' % error.message line run, but I think you wanted to use error.content there instead; that's the attribute HttpError() sets from the second argument, at any rate.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 10
    the `side_effect` is the key part – Daniel Butler Apr 17 '19 at 13:40
  • What does HttpError(mock.Mock(status=404), 'not found') do? For me, it didn't set the status code to 404, it's still None. – Yang Nov 16 '20 at 12:37
  • 1
    @Yang: are you using the [`google-api-client` exception class](https://github.com/googleapis/google-api-python-client/blob/25fba648ea647b62f2a6edc54ae927c1ed381b45/googleapiclient/errors.py#L35-L86)? I can't help you without any detail on what exactly you are doing. The `mock.Mock(status=404)` expression creates a mock object that has a `.status` attribute, which `HttpError` stores as the `resp` attribute. In the question, that is then accessed as `error.resp.status`. – Martijn Pieters Nov 16 '20 at 13:15