Mock 在 Python 中的使用介绍

原文链接与说明

  1. https://www.toptal.com/python/an-introduction-to-mocking-in-python
  2. 本翻译文档原文选题自 Linux中国 ,翻译文档版权归属 Linux中国 所有

本文讲述的是 Python 中 Mock 的使用

如何在避免测试你的耐心的情况下执行单元测试

很多时候,我们编写的软件会直接与那些被标记为肮脏无比的服务交互。用外行人的话说:交互已设计好的服务对我们的应用程序很重要,但是这会给我们带来不希望的副作用,也就是那些在一个自动化测试运行的上下文中不希望的功能。

例如:我们正在写一个社交 app,并且想要测试一下 "发布到 Facebook" 的新功能,但是不想每次运行测试集的时候真的发布到 Facebook。

Python 的 unittest 库包含了一个名为 unittest.mock 或者可以称之为依赖的子包,简称为

mock —— 其提供了极其强大和有用的方法,通过它们可以模拟和打桩来去除我们不希望的副作用。

注意:mock 最近收录到了 Python 3.3 的标准库中;先前发布的版本必须通过 PyPI 下载 Mock 库。

恐惧系统调用

再举另一个例子,思考一个我们会在余文讨论的系统调用。不难发现,这些系统调用都是主要的模拟对象:无论你是正在写一个可以弹出 CD 驱动的脚本,还是一个用来删除 /tmp 下过期的缓存文件的 Web 服务,或者一个绑定到 TCP 端口的 socket 服务器,这些调用都是在你的单元测试上下文中不希望的副作用。

作为一个开发者,你需要更关心你的库是否成功地调用了一个可以弹出 CD 的系统函数,而不是切身经历 CD 托盘每次在测试执行的时候都打开了。

作为一个开发者,你需要更关心你的库是否成功地调用了一个可以弹出 CD 的系统函数(使用了正确的参数等等),而不是切身经历 CD 托盘每次在测试执行的时候都打开了。(或者更糟糕的是,很多次,在一个单元测试运行期间多个测试都引用了弹出代码!)

同样,保持单元测试的效率和性能意味着需要让如此多的 "缓慢代码" 远离自动测试,比如文件系统和网络访问。

对于首个例子,我们要从原始形式到使用 mock 重构一个标准 Python 测试用例。我们会演示如何使用 mock 写一个测试用例,使我们的测试更加智能、快速,并展示更多关于我们软件的工作原理。

一个简单的删除函数

有时,我们都需要从文件系统中删除文件,因此,让我们在 Python 中写一个可以使我们的脚本更加轻易完成此功能的函数。

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. def rm(filename):
  5. os.remove(filename)

很明显,我们的 rm 方法此时无法提供比 os.remove 方法更多的相关功能,但我们可以在这里添加更多的功能,使我们的基础代码逐步改善。

让我们写一个传统的测试用例,即,没有使用 mock

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from mymodule import rm
  4. import os.path
  5. import tempfile
  6. import unittest
  7. class RmTestCase(unittest.TestCase):
  8. tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile")
  9. def setUp(self):
  10. with open(self.tmpfilepath, "wb") as f:
  11. f.write("Delete me!")
  12. def test_rm(self):
  13. # remove the file
  14. rm(self.tmpfilepath)
  15. # test that it was actually removed
  16. self.assertFalse(os.path.isfile(self.tmpfilepath), "Failed to remove the file.")

我们的测试用例相当简单,但是在它每次运行的时候,它都会创建一个临时文件并且随后删除。此外,我们没有办法测试我们的 rm 方法是否正确地将我们的参数向下传递给 os.remove 调用。我们可以基于以上的测试认为它做到了,但还有很多需要改进的地方。

使用 Mock 重构

让我们使用 mock 重构我们的测试用例:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from mymodule import rm
  4. import mock
  5. import unittest
  6. class RmTestCase(unittest.TestCase):
  7. @mock.patch('mymodule.os')
  8. def test_rm(self, mock_os):
  9. rm("any path")
  10. # test that rm called os.remove with the right parameters
  11. mock_os.remove.assert_called_with("any path")

使用这些重构,我们从根本上改变了测试用例的操作方式。现在,我们有一个可以用于验证其他功能的内部对象。

潜在陷阱

第一件需要注意的事情就是,我们使用了 mock.patch 方法装饰器,用于模拟位于 mymodule.os 的对象,并且将 mock 注入到我们的测试用例方法。那么只是模拟 os 本身,而不是 mymodule.osos 的引用(注意 @mock.patch('mymodule.os') 便是模拟 mymodule.os 下的 os,译者注),会不会更有意义呢?

当然,当涉及到导入和管理模块,Python 的用法非常灵活。在运行时,mymodule 模块拥有被导入到本模块局部作用域的 os。因此,如果我们模拟 os,我们是看不到 mock 在 mymodule 模块中的作用的。

这句话需要深刻地记住:

模拟测试一个项目,只需要了解它用在哪里,而不是它从哪里来。

如果你需要为 myproject.app.MyElaborateClass 模拟 tempfile 模块,你可能需要将 mock 用于 myproject.app.tempfile,而其他模块保持自己的导入。

先将那个陷阱置身事外,让我们继续模拟。

向 ‘rm’ 中加入验证

之前定义的 rm 方法相当的简单。在盲目地删除之前,我们倾向于验证一个路径是否存在,并验证其是否是一个文件。让我们重构 rm 使其变得更加智能:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import os.path
  5. def rm(filename):
  6. if os.path.isfile(filename):
  7. os.remove(filename)

很好。现在,让我们调整测试用例来保持测试的覆盖率。

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from mymodule import rm
  4. import mock
  5. import unittest
  6. class RmTestCase(unittest.TestCase):
  7. @mock.patch('mymodule.os.path')
  8. @mock.patch('mymodule.os')
  9. def test_rm(self, mock_os, mock_path):
  10. # set up the mock
  11. mock_path.isfile.return_value = False
  12. rm("any path")
  13. # test that the remove call was NOT called.
  14. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")
  15. # make the file 'exist'
  16. mock_path.isfile.return_value = True
  17. rm("any path")
  18. mock_os.remove.assert_called_with("any path")

我们的测试用例完全改变了。现在我们可以在没有任何副作用下核实并验证方法的内部功能。

将文件删除作为服务

到目前为止,我们只是将 mock 应用在函数上,并没应用在需要传递参数的对象和实例的方法。我们现在开始涵盖对象的方法。

首先,我们将 rm 方法重构成一个服务类。实际上将这样一个简单的函数转换成一个对象,在本质上这不是一个合理的需求,但它能够帮助我们了解 mock 的关键概念。让我们开始重构:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import os.path
  5. class RemovalService(object):
  6. """A service for removing objects from the filesystem."""
  7. def rm(filename):
  8. if os.path.isfile(filename):
  9. os.remove(filename)

你会注意到我们的测试用例没有太大变化:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from mymodule import RemovalService
  4. import mock
  5. import unittest
  6. class RemovalServiceTestCase(unittest.TestCase):
  7. @mock.patch('mymodule.os.path')
  8. @mock.patch('mymodule.os')
  9. def test_rm(self, mock_os, mock_path):
  10. # instantiate our service
  11. reference = RemovalService()
  12. # set up the mock
  13. mock_path.isfile.return_value = False
  14. reference.rm("any path")
  15. # test that the remove call was NOT called.
  16. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")
  17. # make the file 'exist'
  18. mock_path.isfile.return_value = True
  19. reference.rm("any path")
  20. mock_os.remove.assert_called_with("any path")

很好,我们知道 RemovalService 会如期工作。接下来让我们创建另一个服务,将 RemovalService 声明为它的一个依赖:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import os.path
  5. class RemovalService(object):
  6. """A service for removing objects from the filesystem."""
  7. def rm(self, filename):
  8. if os.path.isfile(filename):
  9. os.remove(filename)
  10. class UploadService(object):
  11. def __init__(self, removal_service):
  12. self.removal_service = removal_service
  13. def upload_complete(self, filename):
  14. self.removal_service.rm(filename)

因为我们的测试覆盖了 RemovalService,因此我们不会对我们测试用例中 UploadService 的内部函数 rm 进行验证。相反,我们将调用 UploadServiceRemovalService.rm 方法来进行简单测试(当然没有其他副作用),我们通过之前的测试用例便能知道它可以正确地工作。

这里有两种方法来实现测试:

  1. 模拟 RemovalService.rm 方法本身。
  2. 在 UploadService 的构造函数中提供一个模拟实例。

因为这两种方法都是单元测试中非常重要的方法,所以我们将同时对这两种方法进行回顾。

方法 1:模拟实例的方法

mock 库有一个特殊的方法装饰器,可以模拟对象实例的方法和属性,即 @mock.patch.object decorator 装饰器:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from mymodule import RemovalService, UploadService
  4. import mock
  5. import unittest
  6. class RemovalServiceTestCase(unittest.TestCase):
  7. @mock.patch('mymodule.os.path')
  8. @mock.patch('mymodule.os')
  9. def test_rm(self, mock_os, mock_path):
  10. # instantiate our service
  11. reference = RemovalService()
  12. # set up the mock
  13. mock_path.isfile.return_value = False
  14. reference.rm("any path")
  15. # test that the remove call was NOT called.
  16. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")
  17. # make the file 'exist'
  18. mock_path.isfile.return_value = True
  19. reference.rm("any path")
  20. mock_os.remove.assert_called_with("any path")
  21. class UploadServiceTestCase(unittest.TestCase):
  22. @mock.patch.object(RemovalService, 'rm')
  23. def test_upload_complete(self, mock_rm):
  24. # build our dependencies
  25. removal_service = RemovalService()
  26. reference = UploadService(removal_service)
  27. # call upload_complete, which should, in turn, call `rm`:
  28. reference.upload_complete("my uploaded file")
  29. # check that it called the rm method of any RemovalService
  30. mock_rm.assert_called_with("my uploaded file")
  31. # check that it called the rm method of _our_ removal_service
  32. removal_service.rm.assert_called_with("my uploaded file")

非常棒!我们验证了 UploadService 成功调用了我们实例的 rm 方法。你是否注意到一些有趣的地方?这种修补机制(patching mechanism)实际上替换了我们测试用例中的所有 RemovalService 实例的 rm 方法。这意味着我们可以检查实例本身。如果你想要了解更多,可以试着在你模拟的代码下断点,以对这种修补机制的原理获得更好的认识。

陷阱:装饰顺序

当我们在测试方法中使用多个装饰器,其顺序是很重要的,并且很容易混乱。基本上,当装饰器被映射到方法参数时,装饰器的工作顺序是反向的。思考这个例子:

  1. @mock.patch('mymodule.sys')
  2. @mock.patch('mymodule.os')
  3. @mock.patch('mymodule.os.path')
  4. def test_something(self, mock_os_path, mock_os, mock_sys):
  5. pass

注意到我们的参数和装饰器的顺序是反向匹配了吗?这多多少少是由 Python 的工作方式 导致的。这里是使用多个装饰器的情况下它们执行顺序的伪代码:

  1. patch_sys(patch_os(patch_os_path(test_something)))

因为 sys 补丁位于最外层,所以它最晚执行,使得它成为实际测试方法参数的最后一个参数。请特别注意这一点,并且在运行你的测试用例时,使用调试器来保证正确的参数以正确的顺序注入。

方法 2:创建 Mock 实例

我们可以使用构造函数为 UploadService 提供一个 Mock 实例,而不是模拟特定的实例方法。我更推荐方法 1,因为它更加精确,但在多数情况,方法 2 或许更加有效和必要。让我们再次重构测试用例:

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from mymodule import RemovalService, UploadService
  4. import mock
  5. import unittest
  6. class RemovalServiceTestCase(unittest.TestCase):
  7. @mock.patch('mymodule.os.path')
  8. @mock.patch('mymodule.os')
  9. def test_rm(self, mock_os, mock_path):
  10. # instantiate our service
  11. reference = RemovalService()
  12. # set up the mock
  13. mock_path.isfile.return_value = False
  14. reference.rm("any path")
  15. # test that the remove call was NOT called.
  16. self.assertFalse(mock_os.remove.called, "Failed to not remove the file if not present.")
  17. # make the file 'exist'
  18. mock_path.isfile.return_value = True
  19. reference.rm("any path")
  20. mock_os.remove.assert_called_with("any path")
  21. class UploadServiceTestCase(unittest.TestCase):
  22. def test_upload_complete(self, mock_rm):
  23. # build our dependencies
  24. mock_removal_service = mock.create_autospec(RemovalService)
  25. reference = UploadService(mock_removal_service)
  26. # call upload_complete, which should, in turn, call `rm`:
  27. reference.upload_complete("my uploaded file")
  28. # test that it called the rm method
  29. 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.Mockmock.MagicMock,大多数内部函数都是建立在这两个类之上的。当在选择使用 mock.Mock 实例,mock.MagicMock 实例或 auto-spec 的时候,通常倾向于选择使用 auto-spec,因为对于未来的变化,它更能保持测试的健全。这是因为 mock.Mockmock.MagicMock 会无视底层的 API,接受所有的方法调用和属性赋值。比如下面这个用例:

  1. class Target(object):
  2. def apply(value):
  3. return value
  4. def method(target, value):
  5. return target.apply(value)

我们可以像下面这样使用 mock.Mock 实例进行测试:

  1. class MethodTestCase(unittest.TestCase):
  2. def test_method(self):
  3. target = mock.Mock()
  4. method(target, "value")
  5. target.apply.assert_called_with("value")

这个逻辑看似合理,但如果我们修改 Target.apply 方法接受更多参数:

  1. class Target(object):
  2. def apply(value, are_you_sure):
  3. if are_you_sure:
  4. return value
  5. else:
  6. return None

重新运行你的测试,你会发现它仍能通过。这是因为它不是针对你的 API 创建的。这就是为什么你总是应该使用 create_autospec 方法,并且在使用 @patch@patch.object 装饰方法时使用 autospec 参数。

现实例子:模拟 Facebook API 调用

为了完成,我们写一个更加适用的现实例子,一个在介绍中提及的功能:发布消息到 Facebook。我将写一个不错的包装类及其对应的测试用例。

  1. import facebook
  2. class SimpleFacebook(object):
  3. def __init__(self, oauth_token):
  4. self.graph = facebook.GraphAPI(oauth_token)
  5. def post_message(self, message):
  6. """Posts a message to the Facebook wall."""
  7. self.graph.put_object("me", "feed", message=message)

这是我们的测试用例,它可以检查我们发布的消息,而不是真正地发布消息:

  1. import facebook
  2. import simple_facebook
  3. import mock
  4. import unittest
  5. class SimpleFacebookTestCase(unittest.TestCase):
  6. @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True)
  7. def test_post_message(self, mock_put_object):
  8. sf = simple_facebook.SimpleFacebook("fake oauth token")
  9. sf.post_message("Hello World!")
  10. # verify
  11. mock_put_object.assert_called_with(message="Hello World!")

正如我们所看到的,在 Python 中,通过 mock,我们可以非常容易地动手写一个更加智能的测试用例。

Python Mock 总结

单元测试 来说,Python 的 mock 库可以说是一个游戏变革者,即使对于它的使用还有点困惑。我们已经演示了单元测试中常见的用例以开始使用 mock,并希望这篇文章能够帮助 Python 开发者 克服初期的障碍,写出优秀、经受过考验的代码。


via: https://www.toptal.com/python/an-introduction-to-mocking-in-python

[翻译]Mock 在 Python 中的使用介绍的更多相关文章

  1. Python中__init__方法介绍

    本文介绍Python中__init__方法的意义.         __init__方法在类的一个对象被建立时,马上运行.这个方法可以用来对你的对象做一些你希望的 初始化 .注意,这个名称的开始和结尾 ...

  2. Python中的模块介绍和使用

    在Python中有一个概念叫做模块(module),这个和C语言中的头文件以及Java中的包很类似,比如在Python中要调用sqrt函数,必须用import关键字引入math这个模块,下面就来了解一 ...

  3. Python中的函数介绍

    调用函数 python中有很多内置函数,我们可以直接调用,内置函数能直接在官网查看:https://docs.python.org/3/library/functions.html#abs 定义函数 ...

  4. Python 中lambda 简单介绍

    转自:https://www.cnblogs.com/AlwaysWIN/p/6202320.html 在学习python的过程中,lambda的语法经常出现,现在将它整理一下,以备日后查看. 1.l ...

  5. 『无为则无心』Python面向对象 — 53、对Python中封装的介绍

    目录 1.继承的概念 2.继承的好处 3.继承体验 4.单继承 5.多继承 1.继承的概念 在Python中,如果两个类存在父子级别的继承关系,子类中即便没有任何属性和方法,此时创建一个子类对象,那么 ...

  6. python 中 urlparse 模块介绍

    urlparse模块主要是用于解析url中的参数  对url按照一定格式进行 拆分或拼接 1.urlparse.urlparse 将url分为6个部分,返回一个包含6个字符串项目的元组:协议.位置.路 ...

  7. python中join()方法介绍

    描述 Python join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串. 语法 join()方法语法:str.join(sequence) 参数 sequence -- 要连接的元 ...

  8. python中sort命令介绍以及list结构中统计各元素出现的个数的方法

  9. 三十七、python中的logging介绍

    A.单文件日志 import logging#定义日志文件#文件格式logging.basicConfig(filename='log.log', format='%(asctime)s-%(name ...

随机推荐

  1. 树链剖分的一种妙用与一类树链修改单点查询问题的时间复杂度优化——2018ACM陕西邀请赛J题

    题目描述 有一棵树,每个结点有一个灯(初始均是关着的).每个灯能对该位置和相邻结点贡献1的亮度.现有两种操作: (1)将一条链上的灯状态翻转,开变关.关变开: (2)查询一个结点的亮度. 数据规模:\ ...

  2. 三元运算符 与 return

    有三元运算符可以很好的代替if else简单语句 但是在使用的时候发现 与 return使用的时候 需要用这种形式 错误形式: $a ? return 1 ? return 0; 正确形式: retu ...

  3. struts2 上传与下载

    1.Struts.xml <action name="addfileAction" class="Action.addfileAction"> &l ...

  4. Windows环境下消息中间件RabbitMq的搭建与应用

    前言 消息中间件目前已经在很多大型的项目上得到了运用,我们常见的有 RabbitMq, activitymq,kafka,rocketmq,其中rocketmq是阿里自己在kafka的基础上用java ...

  5. 零基础怎么学java

    首先告诉你的是,作为一个初学者想转行学习Java并不是很容易,Java本身是具有一定难度的,虽然说兴趣这东西可以让我们学习不累,但是有多少人学习是因为兴趣,或者有多少人知道自己的兴趣在哪?所以我很明确 ...

  6. 『Lucas定理以及拓展Lucas』

    Lucas定理 在『组合数学基础』中,我们已经提出了\(Lucas\)定理,并给出了\(Lucas\)定理的证明,本文仅将简单回顾,并给出代码. \(Lucas\)定理:当\(p\)为质数时,\(C_ ...

  7. 搜狗输入法与VS快捷键有冲突_处理办法

    前言:搜狗输入法是大家常用的文字输入工具,但是在开启输入法的时候,VS的一些快捷键无法正常使用,如智能提示快捷键:Ctrl+.,这就非常尴尬了,除非把输入法切换成英文或者卸载搜狗改别的输入法,一个是切 ...

  8. SharePoint布局页创建(实战)

    分享人:广州华软 极简 一. 前言 SharePoint有母版页及布局页,母版页控制页面头部.底部,而布局页则控制页面中间内容区域.通过布局页,可以快速修改页面内容区域. SharePoint的页面布 ...

  9. 衡量GDP,哪种夜间灯光数据更靠谱?

    <新科学家>杂志报道,随着经济发展,一些国家通常会新修道路,扩展居民区,这两项措施都会使从太空中看到的灯光强度增加.不少学者利用夜间灯光数据与国内生产总值统计数据进行比较,发现从太空中看到 ...

  10. Ubuntu安装apache+Yii2

    1.下载Yii2 https://www.yiichina.com/download 2.将解压后的文件放在指定的位置,这里是/home/www/yii/ 3.安装apache2 sudo apt-g ...