目录html
本文讲述的是 Python 中 Mock 的使用python
如何在避免测试你的耐心的状况下执行单元测试api
不少时候,咱们编写的软件会直接与那些被标记为肮脏无比的服务交互。用外行人的话说:交互已设计好的服务对咱们的应用程序很重要,可是这会给咱们带来不但愿的反作用,也就是那些在一个自动化测试运行的上下文中不但愿的功能。缓存
例如:咱们正在写一个社交 app,而且想要测试一下 "发布到 Facebook" 的新功能,可是不想每次运行测试集的时候真的发布到 Facebook。服务器
Python 的 unittest
库包含了一个名为 unittest.mock
或者能够称之为依赖的子包,简称为
mock
—— 其提供了极其强大和有用的方法,经过它们能够模拟和打桩来去除咱们不但愿的反作用。网络
注意:
mock
最近收录到了 Python 3.3 的标准库中;先前发布的版本必须经过 PyPI 下载 Mock 库。app
再举另外一个例子,思考一个咱们会在余文讨论的系统调用。不难发现,这些系统调用都是主要的模拟对象:不管你是正在写一个能够弹出 CD 驱动的脚本,仍是一个用来删除 /tmp 下过时的缓存文件的 Web 服务,或者一个绑定到 TCP 端口的 socket 服务器,这些调用都是在你的单元测试上下文中不但愿的反作用。socket
做为一个开发者,你须要更关心你的库是否成功地调用了一个能够弹出 CD 的系统函数,而不是切身经历 CD 托盘每次在测试执行的时候都打开了。函数
做为一个开发者,你须要更关心你的库是否成功地调用了一个能够弹出 CD 的系统函数(使用了正确的参数等等),而不是切身经历 CD 托盘每次在测试执行的时候都打开了。(或者更糟糕的是,不少次,在一个单元测试运行期间多个测试都引用了弹出代码!)post
一样,保持单元测试的效率和性能意味着须要让如此多的 "缓慢代码" 远离自动测试,好比文件系统和网络访问。
对于首个例子,咱们要从原始形式到使用 mock
重构一个标准 Python 测试用例。咱们会演示如何使用 mock 写一个测试用例,使咱们的测试更加智能、快速,并展现更多关于咱们软件的工做原理。
有时,咱们都须要从文件系统中删除文件,所以,让咱们在 Python 中写一个可使咱们的脚本更加轻易完成此功能的函数。
#!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)
很明显,咱们的 rm
方法此时没法提供比 os.remove
方法更多的相关功能,但咱们能够在这里添加更多的功能,使咱们的基础代码逐步改善。
让咱们写一个传统的测试用例,即,没有使用 mock
:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import os.path import tempfile import unittest class RmTestCase(unittest.TestCase): tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile") def setUp(self): with open(self.tmpfilepath, "wb") as f: f.write("Delete me!") def test_rm(self): # remove the file rm(self.tmpfilepath) # test that it was actually removed self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.")
咱们的测试用例至关简单,可是在它每次运行的时候,它都会建立一个临时文件而且随后删除。此外,咱们没有办法测试咱们的 rm
方法是否正确地将咱们的参数向下传递给 os.remove
调用。咱们能够基于以上的测试认为它作到了,但还有不少须要改进的地方。
让咱们使用 mock 重构咱们的测试用例:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm("any path") # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with("any path")
使用这些重构,咱们从根本上改变了测试用例的操做方式。如今,咱们有一个能够用于验证其余功能的内部对象。
第一件须要注意的事情就是,咱们使用了 mock.patch
方法装饰器,用于模拟位于 mymodule.os
的对象,而且将 mock 注入到咱们的测试用例方法。那么只是模拟 os
自己,而不是 mymodule.os
下 os
的引用(注意 @mock.patch('mymodule.os')
即是模拟 mymodule.os
下的 os
,译者注),会不会更有意义呢?
固然,当涉及到导入和管理模块,Python 的用法很是灵活。在运行时,mymodule
模块拥有被导入到本模块局部做用域的 os
。所以,若是咱们模拟 os
,咱们是看不到 mock 在 mymodule
模块中的做用的。
这句话须要深入地记住:
模拟测试一个项目,只须要了解它用在哪里,而不是它从哪里来。
若是你须要为 myproject.app.MyElaborateClass
模拟 tempfile
模块,你可能须要将 mock 用于 myproject.app.tempfile
,而其余模块保持本身的导入。
先将那个陷阱置身事外,让咱们继续模拟。
以前定义的 rm 方法至关的简单。在盲目地删除以前,咱们倾向于验证一个路径是否存在,并验证其是不是一个文件。让咱们重构 rm 使其变得更加智能:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path def rm(filename): if os.path.isfile(filename): os.remove(filename)
很好。如今,让咱们调整测试用例来保持测试的覆盖率。
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # set up the mock mock_path.isfile.return_value = False 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 rm("any path") mock_os.remove.assert_called_with("any path")
咱们的测试用例彻底改变了。如今咱们能够在没有任何反作用下核实并验证方法的内部功能。
到目前为止,咱们只是将 mock 应用在函数上,并没应用在须要传递参数的对象和实例的方法。咱们如今开始涵盖对象的方法。
首先,咱们将 rm
方法重构成一个服务类。实际上将这样一个简单的函数转换成一个对象,在本质上这不是一个合理的需求,但它可以帮助咱们了解 mock
的关键概念。让咱们开始重构:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): """A service for removing objects from the filesystem.""" def rm(filename): if os.path.isfile(filename): os.remove(filename)
你会注意到咱们的测试用例没有太大变化:
#!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService 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")
很好,咱们知道 RemovalService
会如期工做。接下来让咱们建立另外一个服务,将 RemovalService
声明为它的一个依赖:
:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): """A service for removing objects from the filesystem.""" def rm(self, filename): if os.path.isfile(filename): os.remove(filename) class UploadService(object): def __init__(self, removal_service): self.removal_service = removal_service def upload_complete(self, filename): self.removal_service.rm(filename)
由于咱们的测试覆盖了 RemovalService
,所以咱们不会对咱们测试用例中 UploadService
的内部函数 rm
进行验证。相反,咱们将调用 UploadService
的 RemovalService.rm
方法来进行简单测试(固然没有其余反作用),咱们经过以前的测试用例便能知道它能够正确地工做。
这里有两种方法来实现测试:
由于这两种方法都是单元测试中很是重要的方法,因此咱们将同时对这两种方法进行回顾。
mock
库有一个特殊的方法装饰器,能够模拟对象实例的方法和属性,即 @mock.patch.object decorator
装饰器:
#!/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): @mock.patch.object(RemovalService, 'rm') def test_upload_complete(self, mock_rm): # build our dependencies removal_service = RemovalService() reference = UploadService(removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete("my uploaded file") # check that it called the rm method of any RemovalService mock_rm.assert_called_with("my uploaded file") # check that it called the rm method of _our_ removal_service removal_service.rm.assert_called_with("my uploaded file")
很是棒!咱们验证了 UploadService 成功调用了咱们实例的 rm 方法。你是否注意到一些有趣的地方?这种修补机制(patching mechanism)实际上替换了咱们测试用例中的全部 RemovalService
实例的 rm
方法。这意味着咱们能够检查实例自己。若是你想要了解更多,能够试着在你模拟的代码下断点,以对这种修补机制的原理得到更好的认识。
当咱们在测试方法中使用多个装饰器,其顺序是很重要的,而且很容易混乱。基本上,当装饰器被映射到方法参数时,装饰器的工做顺序是反向的。思考这个例子:
@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 补丁位于最外层,因此它最晚执行,使得它成为实际测试方法参数的最后一个参数。请特别注意这一点,而且在运行你的测试用例时,使用调试器来保证正确的参数以正确的顺序注入。
咱们可使用构造函数为 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 和 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。我将写一个不错的包装类及其对应的测试用例。
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
库能够说是一个游戏变革者,即便对于它的使用还有点困惑。咱们已经演示了单元测试中常见的用例以开始使用 mock
,并但愿这篇文章可以帮助 Python 开发者 克服初期的障碍,写出优秀、经受过考验的代码。
via: https://www.toptal.com/python/an-introduction-to-mocking-in-python