python - 我如何使用 unittest.mock 来消除代码中的副作用?

标签 python unit-testing python-3.x exception python-unittest.mock

我有一个有几个故障点的函数:

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 实际上是一个有效/可访问的 URL?

我为 Foo 创建了一个模拟类:

class MockFoo(Foo):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

我的想法是模拟对 setup_foo 的调用并消除引发 UnreachableEndpoint 异常的副作用。我知道如何使用 unittest.mockMock添加 副作用,但我该如何移除它们?

最佳答案

假设您的异常是从 foo.authenticate() 引发的,您在这里想要意识到的是,数据实际上是否真的并不一定重要在你的测试中有效。你真正想说的是:

当这个外部方法引发某些事情时,我的代码应该根据那个事情做出相应的行为。

因此,考虑到这一点,您要做的是采用不同的测试方法,在这些方法中传递应该 的有效数据,并让您的代码做出相应的 react 。数据本身并不重要,但它提供了一种文档化的方式来显示代码应该如何处理以这种方式传递的数据。

最终,你不应该关心nova客户端如何处理你给它的数据(nova客户端已经过测试,你不应该关心它)。你关心的是它回馈给你什么,以及你想如何处理它,而不管你给了它什么。

换句话说,为了您的测试,您实际上可以传递一个虚拟 url 作为:

"this_is_a_dummy_url_that_works"

为了您的测试,您可以让它通过,因为在您的 mock 中,您将相应地提高。

例如。你在这里应该做的实际上是从 novaclient 中模拟出 Client。有了这个模拟,您现在可以在 novaclient 中操纵任何调用,这样您就可以正确地测试您的代码。

这实际上让我们找到了问题的根源。您的第一个异常(exception)是捕获以下内容:

except (OSError, NotFound, ClientException)

这里的问题是,您现在正在捕获 ClientExceptionnovaclient 中的几乎所有异常都继承自 ClientException,因此无论您尝试在该异常行之外测试什么,您都永远不会遇到这些异常。您在这里有两个选择。捕获 ClientException,并引发自定义异常,或者远程 ClientException,并更明确(就像您已经这样做的那样)。

因此,让我们着手移除 ClientException 并相应地设置我们的示例。

因此,在您的真实代码中,您现在应该将第一个异常行设置为:

except (OSError, NotFound) as e:

此外,您遇到的下一个问题是您没有正确模拟。你应该模拟你正在测试的地方。因此,如果您的 setup_nova 方法位于名为 your_nova_module 的模块中。就此而言,您应该 mock 。下面的例子说明了这一切。

@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)

因此,上面示例的主要思想是,传递什么数据并不重要。真正重要的是您想知道当外部方法引发时您的代码将如何 react 。

因此,我们的目标是实际提出一些东西,让您的第二个异常处理程序得到测试:Unauthorized

此代码已针对您在问题中发布的代码进行了测试。唯一的修改是模块名称以反射(reflect)我的环境。

关于python - 我如何使用 unittest.mock 来消除代码中的副作用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39919699/

相关文章:

python - 轻型 GBM 提前停止不适用于自定义指标

unit-testing - 如何让 Selenium 服务器与 htmlunit 一起工作?

python-3.x - redis-py 在退出时不关闭线程

python - 尝试使用 USB 将 Nintendo Switch 与 Raspberry 3b+ 进行通信

python - Python 中的数据格式化和操作

python - 在 Windows 8.1 上安装 lxml、libxml2、libxslt

python - 对重复出现的行进行分组并从 Pandas 中的单个日期时间列中查找时间差

unit-testing - 如何获得可以抛出我正在使用 XCTAssertNoThrow(...) 测试的函数的返回值

unit-testing - 自动测试经典 ASP

python - 如何更换窗口TKInter