我正在处理一个涉及连接到远程服务器,等待响应,然后根据该响应执行操作的项目。我们捕获了几个不同的异常,并且根据捕获的异常而表现不同。例如:
def myMethod(address, timeout=20):
try:
response = requests.head(address, timeout=timeout)
except requests.exceptions.Timeout:
# do something special
except requests.exceptions.ConnectionError:
# do something special
except requests.exceptions.HTTPError:
# do something special
else:
if response.status_code != requests.codes.ok:
# do something special
return successfulConnection.SUCCESS
为了测试这一点,我们编写了类似以下的测试
class TestMyMethod(unittest.TestCase):
def test_good_connection(self):
config = {
'head.return_value': type('MockResponse', (), {'status_code': requests.codes.ok}),
'codes.ok': requests.codes.ok
}
with mock.patch('path.to.my.package.requests', **config):
self.assertEqual(
mypackage.myMethod('some_address',
mypackage.successfulConnection.SUCCESS
)
def test_bad_connection(self):
config = {
'head.side_effect': requests.exceptions.ConnectionError,
'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError
}
with mock.patch('path.to.my.package.requests', **config):
self.assertEqual(
mypackage.myMethod('some_address',
mypackage.successfulConnection.FAILURE
)
如果我直接运行该功能,一切都按预期发生。我甚至通过将raise requests.exceptions.ConnectionError
添加到函数的try
子句来进行测试。但是当我进行单元测试时,我得到了
ERROR: test_bad_connection (test.test_file.TestMyMethod)
----------------------------------------------------------------
Traceback (most recent call last):
File "path/to/sourcefile", line ###, in myMethod
respone = requests.head(address, timeout=timeout)
File "path/to/unittest/mock", line 846, in __call__
return _mock_self.mock_call(*args, **kwargs)
File "path/to/unittest/mock", line 901, in _mock_call
raise effect
my.package.requests.exceptions.ConnectionError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "Path/to/my/test", line ##, in test_bad_connection
mypackage.myMethod('some_address',
File "Path/to/package", line ##, in myMethod
except requests.exceptions.ConnectionError:
TypeError: catching classes that do not inherit from BaseException is not allowed
我尝试将我正在修补的异常更改为BaseException
,但我得到了或多或少相同的错误。
我已经阅读了https://stackoverflow.com/a/18163759/3076272,所以我认为它必定是一个错误的__del__
钩子,但我不确定在哪里寻找它或我甚至可以同时做。我对unittest.mock.patch()
也相对较新,所以我很有可能在那里做错了。
这是一个Fusion360插件,因此它使用的是Fusion 360的Python 3.3打包版本 - 据我所知它是一个普通版本(即他们不会推出自己的版本)但我并不是肯定的。
答案 0 :(得分:27)
我可以用最小的例子重现错误:
foo.py:
class MyError(Exception):
pass
class A:
def inner(self):
err = MyError("FOO")
print(type(err))
raise err
def outer(self):
try:
self.inner()
except MyError as err:
print ("catched ", err)
return "OK"
无需嘲笑地进行测试:
class FooTest(unittest.TestCase):
def test_inner(self):
a = foo.A()
self.assertRaises(foo.MyError, a.inner)
def test_outer(self):
a = foo.A()
self.assertEquals("OK", a.outer())
好的,一切都很好,两次测试通过
问题来自嘲笑。一旦MyError类被模拟,expect
子句就无法捕获任何内容,我会得到与问题中的示例相同的错误:
class FooTest(unittest.TestCase):
def test_inner(self):
a = foo.A()
self.assertRaises(foo.MyError, a.inner)
def test_outer(self):
with unittest.mock.patch('foo.MyError'):
a = exc2.A()
self.assertEquals("OK", a.outer())
立即给出:
ERROR: test_outer (__main__.FooTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "...\foo.py", line 11, in outer
self.inner()
File "...\foo.py", line 8, in inner
raise err
TypeError: exceptions must derive from BaseException
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<pyshell#78>", line 8, in test_outer
File "...\foo.py", line 12, in outer
except MyError as err:
TypeError: catching classes that do not inherit from BaseException is not allowed
在这里,我得到了你没有的第一个TypeError
,因为我在配置中强制'requests.exceptions.ConnectionError': requests.exceptions.ConnectionError
的真正异常的同时提高了模拟效果。但问题仍然是 except
子句试图捕捉模拟。
TL / DR:当您模拟完整的requests
包时,except requests.exceptions.ConnectionError
子句会尝试捕获模拟。由于模拟不是真正的BaseException
,它会导致错误。
我能想象的唯一解决方案不是模拟完整的requests
,而只模仿非例外的部分。我必须承认我无法找到如何模仿模拟除了之外的所有内容,但在您的示例中,您只需要修补requests.head
。所以我认为这应该有效:
def test_bad_connection(self):
with mock.patch('path.to.my.package.requests.head',
side_effect=requests.exceptions.ConnectionError):
self.assertEqual(
mypackage.myMethod('some_address',
mypackage.successfulConnection.FAILURE
)
即:仅修补head
方法,副作用为异常。
答案 1 :(得分:4)
我在尝试模拟sqlite3
时遇到了同样的问题(并在寻找解决方案时发现了这篇文章)。
Serge说的是正确的:
TL / DR:当您模拟完整的请求包时,except requests.exceptions.ConnectionError子句尝试捕获模拟。由于mock不是真正的BaseException,它会导致错误。
我能想象的唯一解决方案不是模拟完整的请求,而只模拟非例外的部分。我必须承认我无法找到如何模仿模拟除了
之外的所有内容
我的解决方案是模拟整个模块,然后将异常的mock属性设置为等于真实类中的异常,有效地“取消模拟”异常。例如,在我的情况下:
@mock.patch(MyClass.sqlite3)
def test_connect_fail(self, mock_sqlite3):
mock_sqlite3.connect.side_effect = sqlite3.OperationalError()
mock_sqlite3.OperationalError = sqlite3.OperationalError
self.assertRaises(sqlite3.OperationalError, MyClass, self.db_filename)
对于requests
,您可以单独分配例外:
mock_requests.exceptions.ConnectionError = requests.exceptions.ConnectionError
或对所有requests
例外执行此操作:
mock_requests.exceptions = requests.exceptions
我不知道这是否是“正确”的方式,但到目前为止它似乎对我没有任何问题。
答案 2 :(得分:1)
对于我们这些需要模拟异常并且无法通过简单地修补head
来做到这一点的人来说,这是一个简单的解决方案,用空的替换目标异常:
假设我们有一个通用单元来测试我们必须模拟的异常:
# app/foo_file.py
def test_me():
try:
foo()
return "No foo error happened"
except CustomError: # <-- Mock me!
return "The foo error was caught"
我们想要模拟CustomError
但是因为它是一个例外我们遇到麻烦,如果我们尝试像其他一切一样修补它。通常情况下,对patch
的调用会使用MagicMock
替换目标,但这不会在此处起作用。模拟很漂亮,但它们的行为并不像例外。而不是使用模拟修补,而是让它给它一个存根例外。我们会在测试文件中这样做。
# app/test_foo_file.py
from mock import patch
# A do-nothing exception we are going to replace CustomError with
class StubException(Exception):
pass
# Now apply it to our test
@patch('app.foo_file.foo')
@patch('app.foo_file.CustomError', new_callable=lambda: StubException)
def test_foo(stub_exception, mock_foo):
mock_foo.side_effect = stub_exception("Stub") # Raise our stub to be caught by CustomError
assert test_me() == "The error was caught"
# Success!
那么lambda
是什么? new_callable
param调用我们提供的任何内容,并用该调用的返回替换目标。如果我们直接传递StubException
类,它将调用类的构造函数并使用异常实例而不是类修补我们的目标对象。不是我们想要的。通过用lambda
包装它,它会按照我们的意图返回我们的类。
完成修补后,stub_exception
对象(实际上是我们的StubException
类)可以被引发并被捕获,就像它是CustomError
一样。整齐!
答案 3 :(得分:0)
我在尝试模拟sh包时遇到了类似的问题。虽然sh非常有用,但动态定义所有方法和异常的事实使得模拟它们变得更加困难。因此,遵循documentation:
的建议ID
答案 4 :(得分:0)
我在模仿struct
时遇到了同样的问题。
我收到错误:
TypeError:不允许捕获不从BaseException继承的类
尝试抓住struct.error
时提出的struct.unpack
。
我发现在我的测试中解决这个问题的最简单方法是简单地将mock中的error属性值设置为Exception
。例如
我想测试的方法有这个基本模式:
def some_meth(self):
try:
struct.unpack(fmt, data)
except struct.error:
return False
return True
测试有这个基本模式。
@mock.patch('my_module.struct')
def test_some_meth(self, struct_mock):
'''Explain how some_func should work.'''
struct_mock.error = Exception
self.my_object.some_meth()
struct_mock.unpack.assert_called()
struct_mock.unpack.side_effect = struct_mock.error
self.assertFalse(self.my_object.some_meth()
这类似于@BillB采用的方法,但它当然更简单,因为我不需要在我的测试中添加导入并仍然获得相同的行为。对我来说,这似乎是这里答案中推理的一般思路的逻辑结论。
答案 5 :(得分:0)
使用patch.object
来部分模拟一个类。
我的用例:
import unittest
from unittest import mock
import requests
def test_my_function(self):
response = mock.MagicMock()
response.raise_for_status.side_effect = requests.HTTPError
with mock.patch.object(requests, 'get', return_value=response):
my_function()