我如何使用 unittest.mock 来消除代码的副作用?
How can I use unittest.mock to remove side effects from code?
我有一个函数有几个故障点:
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 UnauthorizedUser("Wrong username or password.")
except UnsupportedVersion as e:
raise Unsupported("We only support Foo API with major version 2")
并且我想测试是否捕获了所有相关的异常(尽管目前处理得不好)。
我有一个通过的初始测试用例:
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)
但是如何让我的测试框架相信 AUTH_URL 实际上是 valid/reachable URL?
我为 Foo
创建了一个模拟 class:
class MockFoo(Foo):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
我的想法是模拟对 setup_foo
的调用并消除引发 UnreachableEndpoint
异常的副作用。我知道如何 添加 副作用到 Mock
和 unittest.mock
,但我怎样才能删除它们?
假设您的异常是从 foo.authenticate()
引发的,您在这里要意识到的是,数据实际上 是否真的 有效并不一定重要你的测试。你真正想说的是:
当这个外部方法引发某些事情时,我的代码应该根据那个事情做出相应的行为。
因此,考虑到这一点,您要做的是采用不同的测试方法,在这些方法中您传递 应该 的数据是有效数据,并让您的代码做出相应的反应。数据本身并不重要,但它提供了一种文档化的方式来显示代码应该如何处理以这种方式传递的数据。
归根结底,你不应该关心nova客户端如何处理你给它的数据(nova客户端已经过测试,你不应该关心它)。你关心的是它回馈给你什么,以及你想如何处理它,而不管你给了它什么。
换句话说,为了您的测试,您实际上可以传递一个虚拟 url 作为:
"this_is_a_dummy_url_that_works"
为了您的测试,您可以让它通过,因为在您的 mock
中,您将相应地加注。
例如。你在这里应该做的实际上是从 novaclient
中模拟出 Client
。有了这个模拟,您现在可以在 novaclient 中操作任何调用,这样您就可以正确地测试您的代码。
这实际上让我们找到了问题的根源。您的第一个例外是捕获以下内容:
except (OSError, NotFound, ClientException)
这里的问题是,您现在正在捕捉 ClientException
。 novaclient
中的几乎每个异常都继承自 ClientException
,因此无论您尝试在该异常行之外测试什么,您都永远不会遇到这些异常。您在这里有两个选择。捕获 ClientException
,然后引发自定义异常,或者,远程 ClientException
,并且更明确(就像你已经是的那样)。
所以,让我们开始删除 ClientException
并相应地设置我们的示例。
因此,在您的 真实 代码中,您现在应该将第一个异常行设置为:
except (OSError, NotFound) as e:
此外,您遇到的下一个问题是您没有正确模拟。你应该模拟你正在测试的地方。因此,如果您的 setup_nova
方法位于名为 your_nova_module
的模块中。就此而言,您应该嘲笑。下面的例子说明了这一切。
@patch("your_nova_module.Client", return_value=Mock())
def test_setup_nova_failing_unauthorized_user(self, mock_client):
dummy_creds = {
'AUTH_URL': 'this_url_is_valid',
'USERNAME': 'my_bad_user. this should fail',
'PASSWORD': 'bad_pass_but_it_does_not_matter_what_this_is',
'VERSION': '2.1',
'PROJECT_ID': 'does_not_matter'
}
mock_nova_client = mock_client.return_value
mock_nova_client.authenticate.side_effect = Unauthorized(401)
with self.assertRaises(UnauthorizedUser):
setup_nova(dummy_creds)
因此,上面示例的主要思想是,传递什么数据并不重要。真正重要的是您想知道当外部方法引发时您的代码将如何反应。
因此,我们的目标是实际提出一些东西,让您的第二个异常处理程序得到测试:Unauthorized
此代码已针对您在问题中发布的代码进行了测试。唯一的修改是模块名称以反映我的环境。
如果您想从伪造的 urls 模拟出 HTTP 服务器,我建议您查看 HTTPretty。它在套接字级别模拟 urls,因此它可以欺骗大多数 Python HTTP 库,使其成为有效的 url.
我建议您对单元测试进行以下设置:
class FooTest(unittest.TestCase):
def setUp(self):
httpretty.register_uri(httpretty.GET, "http://bogus.example.com/v2.0",
body='[{"response": "Valid"}]',
content_type="application/json")
@httpretty.activate
def test_test_case(self):
resp = requests.get("http://bogus.example.com/v2.0")
self.assertEquals(resp.status_code, 200)
请注意,模拟将仅适用于使用 http.activate
装饰器装饰的堆栈,因此它不会泄漏到代码中您不想模拟的其他地方。希望这是有道理的。
我有一个函数有几个故障点:
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 UnauthorizedUser("Wrong username or password.")
except UnsupportedVersion as e:
raise Unsupported("We only support Foo API with major version 2")
并且我想测试是否捕获了所有相关的异常(尽管目前处理得不好)。
我有一个通过的初始测试用例:
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)
但是如何让我的测试框架相信 AUTH_URL 实际上是 valid/reachable URL?
我为 Foo
创建了一个模拟 class:
class MockFoo(Foo):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
我的想法是模拟对 setup_foo
的调用并消除引发 UnreachableEndpoint
异常的副作用。我知道如何 添加 副作用到 Mock
和 unittest.mock
,但我怎样才能删除它们?
假设您的异常是从 foo.authenticate()
引发的,您在这里要意识到的是,数据实际上 是否真的 有效并不一定重要你的测试。你真正想说的是:
当这个外部方法引发某些事情时,我的代码应该根据那个事情做出相应的行为。
因此,考虑到这一点,您要做的是采用不同的测试方法,在这些方法中您传递 应该 的数据是有效数据,并让您的代码做出相应的反应。数据本身并不重要,但它提供了一种文档化的方式来显示代码应该如何处理以这种方式传递的数据。
归根结底,你不应该关心nova客户端如何处理你给它的数据(nova客户端已经过测试,你不应该关心它)。你关心的是它回馈给你什么,以及你想如何处理它,而不管你给了它什么。
换句话说,为了您的测试,您实际上可以传递一个虚拟 url 作为:
"this_is_a_dummy_url_that_works"
为了您的测试,您可以让它通过,因为在您的 mock
中,您将相应地加注。
例如。你在这里应该做的实际上是从 novaclient
中模拟出 Client
。有了这个模拟,您现在可以在 novaclient 中操作任何调用,这样您就可以正确地测试您的代码。
这实际上让我们找到了问题的根源。您的第一个例外是捕获以下内容:
except (OSError, NotFound, ClientException)
这里的问题是,您现在正在捕捉 ClientException
。 novaclient
中的几乎每个异常都继承自 ClientException
,因此无论您尝试在该异常行之外测试什么,您都永远不会遇到这些异常。您在这里有两个选择。捕获 ClientException
,然后引发自定义异常,或者,远程 ClientException
,并且更明确(就像你已经是的那样)。
所以,让我们开始删除 ClientException
并相应地设置我们的示例。
因此,在您的 真实 代码中,您现在应该将第一个异常行设置为:
except (OSError, NotFound) as e:
此外,您遇到的下一个问题是您没有正确模拟。你应该模拟你正在测试的地方。因此,如果您的 setup_nova
方法位于名为 your_nova_module
的模块中。就此而言,您应该嘲笑。下面的例子说明了这一切。
@patch("your_nova_module.Client", return_value=Mock())
def test_setup_nova_failing_unauthorized_user(self, mock_client):
dummy_creds = {
'AUTH_URL': 'this_url_is_valid',
'USERNAME': 'my_bad_user. this should fail',
'PASSWORD': 'bad_pass_but_it_does_not_matter_what_this_is',
'VERSION': '2.1',
'PROJECT_ID': 'does_not_matter'
}
mock_nova_client = mock_client.return_value
mock_nova_client.authenticate.side_effect = Unauthorized(401)
with self.assertRaises(UnauthorizedUser):
setup_nova(dummy_creds)
因此,上面示例的主要思想是,传递什么数据并不重要。真正重要的是您想知道当外部方法引发时您的代码将如何反应。
因此,我们的目标是实际提出一些东西,让您的第二个异常处理程序得到测试:Unauthorized
此代码已针对您在问题中发布的代码进行了测试。唯一的修改是模块名称以反映我的环境。
如果您想从伪造的 urls 模拟出 HTTP 服务器,我建议您查看 HTTPretty。它在套接字级别模拟 urls,因此它可以欺骗大多数 Python HTTP 库,使其成为有效的 url.
我建议您对单元测试进行以下设置:
class FooTest(unittest.TestCase):
def setUp(self):
httpretty.register_uri(httpretty.GET, "http://bogus.example.com/v2.0",
body='[{"response": "Valid"}]',
content_type="application/json")
@httpretty.activate
def test_test_case(self):
resp = requests.get("http://bogus.example.com/v2.0")
self.assertEquals(resp.status_code, 200)
请注意,模拟将仅适用于使用 http.activate
装饰器装饰的堆栈,因此它不会泄漏到代码中您不想模拟的其他地方。希望这是有道理的。