陷阱:装饰顺序
当我们在测试方法中使用多个装饰器,其顺序是很重要的,并且很容易混乱。基本上,当装饰器被映射到方法参数时, 装饰器的工作顺序是反向的 。思考这个例子:
@mock.patch('mymodule.sys')
@mock.patch('mymodule.os')
@mock.patch('mymodule.os.path')
def test_something(self, mock_os_path, mock_os, mock_sys):
pass
注意到我们的参数和装饰器的顺序是反向匹配了吗?这部分是由 Python 的工作方式 所导致的。这里是使用多个装饰器的情况下它们执行顺序的伪代码:
patch_sys(patch_os(patch_os_path(test_something)))
因为 sys 补丁位于最外层,所以它最晚执行,使得它成为实际测试方法参数的最后一个参数。请特别注意这一点,并且在运行你的测试用例时,使用调试器来保证正确的参数以正确的顺序注入。
方法 2:创建 Mock 实例
我们可以使用构造函数为 UploadService 提供一个 Mock 实例,而不是模拟特定的实例方法。我更推荐方法 1,因为它更加精确,但在多数情况,方法 2 或许更加有效和必要。让我们再次重构测试用例:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm("any path") # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.") # make the file 'exist' mock_path.isfile.return_value = True reference.rm("any path") mock_os.remove.assert_called_with("any path") class UploadServiceTestCase(unittest.TestCase): def test_upload_complete(self, mock_rm): # build our dependencies mock_removal_service = mock.create_autospec(RemovalService) reference = UploadService(mock_removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete("my uploaded file") # test that it called the rm method mock_removal_service.rm.assert_called_with("my uploaded file") |
在这个例子中,我们甚至不需要修补任何功能,只需为 RemovalService 类创建一个 auto-spec,然后将实例注入到我们的 UploadService 以验证功能。
mock.create_autospec 方法为类提供了一个同等功能实例。实际上来说,这意味着在使用返回的实例进行交互的时候,如果使用了非法的方式将会引发异常。更具体地说,如果一个方法被调用时的参数数目不正确,将引发一个异常。这对于重构来说是非常重要。当一个库发生变化的时候,中断测试正是所期望的。如果不使用 auto-spec,尽管底层的实现已经被破坏,我们的测试仍然会通过。
陷阱:mock.Mock 和 mock.MagicMock 类
mock 库包含了两个重要的类 mock.Mock 和 mock.MagicMock ,大多数内部函数都是建立在这两个类之上的。当在选择使用 mock.Mock 实例、 mock.MagicMock 实例还是 auto-spec 的时候,通常倾向于选择使用 auto-spec,因为对于未来的变化,它更能保持测试的健全。这是因为 mock.Mock 和 mock.MagicMock 会无视底层的 API,接受所有的方法调用和属性赋值。比如下面这个用例:
class Target(object):
def apply(value):
return value
def method(target, value):
return target.apply(value)
我们可以像下面这样使用 mock.Mock 实例进行测试:
class MethodTestCase(unittest.TestCase):
def test_method(self):
target = mock.Mock()
method(target, "value")
target.apply.assert_called_with("value")
这个逻辑看似合理,但如果我们修改 Target.apply 方法接受更多参数:
class Target(object):
def apply(value, are_you_sure):
if are_you_sure:
return value
else:
return None
重新运行你的测试,你会发现它仍能通过。这是因为它不是针对你的 API 创建的。这就是为什么你总是应该使用 create_autospec 方法,并且在使用 @patch 和 @patch.object 装饰方法时使用 autospec 参数。
现实例子:模拟 Facebook API 调用
作为这篇文章的结束,我们写一个更加适用的现实例子,一个在介绍中提及的功能:发布消息到 Facebook。我将写一个不错的包装类及其对应的测试用例。
import facebook
class SimpleFacebook(object):
def __init__(self, oauth_token):
self.graph = facebook.GraphAPI(oauth_token)
def post_message(self, message):
"""Posts a message to the Facebook wall."""
self.graph.put_object("me", "feed", message=message)
这是我们的测试用例,它可以检查我们发布的消息,而不是真正地发布消息:
import facebook
import simple_facebook
import mock
import unittest
class SimpleFacebookTestCase(unittest.TestCase):
@mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True)
def test_post_message(self, mock_put_object):
sf = simple_facebook.SimpleFacebook("fake oauth token")
sf.post_message("Hello World!")
# verify
mock_put_object.assert_called_with(message="Hello World!")
正如我们所看到的,在 Python 中,通过 mock,我们可以非常容易地动手写一个更加智能的测试用例。
Python Mock 总结
即使对它的使用还有点不太熟悉,对 单元测试 来说,Python 的 mock 库可以说是一个规则改变者。我们已经演示了常见的用例来了解了 mock 在单元测试中的使用,希望这篇文章能够帮助 Python 开发者 克服初期的障碍,写出优秀、经受过考验的代码。