2017年8月25日 星期五

[ Python 常見問題 ] Mocking a function to raise an Exception to test an except block

Source From Here 
Question 
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: 
- test.py 
  1. import unittest  
  2. from unittest import mock  
  3.   
  4. class HttpError(Exception):  
  5.     def __init__(self, result, msg):  
  6.         super(HttpError, self).__init__(msg)  
  7.         self.result = result  
  8.   
  9. class FooTests(unittest.TestCase):  
  10.     @mock.patch('test.bar')  
  11.     def test_foo_shouldReturnResultOfBar_whenBarSucceeds(self, barMock):  
  12.         barMock.return_value = True  
  13.         result = foo()  
  14.         self.assertTrue(result)  # passes  
  15.   
  16.     @mock.patch('test.bar')  
  17.     def test_foo_shouldReturnNone_whenBarRaiseHttpError404(self, barMock):  
  18.         barMock.side_effect = HttpError(mock.Mock(return_value={'status': 404}), 'not found')  
  19.         result = foo()  
  20.         self.assertIsNone(result)  # fails, test raises HttpError  
  21.   
  22.     @mock.patch('test.bar')  
  23.     def test_foo_shouldRaiseHttpError_whenBarRaiseHttpErrorNot404(self, barMock):  
  24.         barMock.side_effect = HttpError(mock.Mock(return_value={'status': 500}), 'error')  
  25.         with self.assertRaises(HttpError):  # passes  
  26.             foo()  
  27.   
  28. def foo():  
  29.     try:  
  30.         result = bar()  
  31.         return result  
  32.     except HttpError as error:  
  33.         if error.resp.status == 404:  
  34.             print('404 - %s' % error.message)  
  35.             return None  
  36.         raise  
  37.   
  38. def bar():  
  39.     raise NotImplementedError()  
Execution output: 
# pytest -s -v test.py 
... 
collected 3 items 

test.py::FooTests::test_foo_shouldRaiseHttpError_whenBarRaiseHttpErrorNot404 FAILED 
test.py::FooTests::test_foo_shouldReturnNone_whenBarRaiseHttpError404 FAILED 
test.py::FooTests::test_foo_shouldReturnResultOfBar_whenBarSucceeds PASSED 
...

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 bar Mock 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? 

How-To 
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: 
  1. barMock.side_effect = HttpError(mock.Mock(status=404), 'not found')  
Additional keyword arguments to Mock() are set as attributes on the resulting object. The modified test.py will look like: 
  1. import unittest  
  2. from unittest import mock  
  3.   
  4. class HttpError(Exception):  
  5.     def __init__(self, resp, msg):  
  6.         super(HttpError, self).__init__(msg)  
  7.         self.resp = resp  
  8.         self.message = msg  
  9.   
  10. class FooTests(unittest.TestCase):  
  11.     @mock.patch('test.bar')  
  12.     def test_foo_shouldReturnResultOfBar_whenBarSucceeds(self, barMock):  
  13.         barMock.return_value = True  
  14.         result = foo()  
  15.         self.assertTrue(result)  # passes  
  16.   
  17.     @mock.patch('test.bar')  
  18.     def test_foo_shouldReturnNone_whenBarRaiseHttpError404(self, barMock):  
  19.         barMock.side_effect = HttpError(mock.Mock(status=404), 'not found')  
  20.         result = foo()  
  21.         self.assertIsNone(result)  # fails, test raises HttpError  
  22.   
  23.     @mock.patch('test.bar')  
  24.     def test_foo_shouldRaiseHttpError_whenBarRaiseHttpErrorNot404(self, barMock):  
  25.         barMock.side_effect = HttpError(mock.Mock(status=500), 'error')  
  26.         with self.assertRaises(HttpError):  # passes  
  27.             foo()  
  28.   
  29. def foo():  
  30.     try:  
  31.         result = bar()  
  32.         return result  
  33.     except HttpError as error:  
  34.         if error.resp.status == 404:  
  35.             print('404 - %s' % error.message)  
  36.             return None  
  37.         raise  
  38.   
  39. def bar():  
  40.     raise NotImplementedError()  
The execution output: 
# pytest -s -v test.py 
... 
test.py::FooTests::test_foo_shouldRaiseHttpError_whenBarRaiseHttpErrorNot404 PASSED 
test.py::FooTests::test_foo_shouldReturnNone_whenBarRaiseHttpError404 404 - not found 
PASSED 
test.py::FooTests::test_foo_shouldReturnResultOfBar_whenBarSucceeds PASSED 
...


沒有留言:

張貼留言

[ Python 文章收集 ] List Comprehensions and Generator Expressions

Source From  Here   Preface   Do you know the difference between the following syntax?  view plain copy to clipboard print ? [x  for ...