利用Python中的mock库对Python代码进行模拟测试
admin
2023-07-31 00:38:04
0

如何不靠耐心测试

通常,我们编写的软件会直接与那些我们称之为“肮脏的”服务交互。通俗地说,服务对我们的应用来说是至关重要的,它们之间的交互是我们设计好的,但这会带来我们不希望的副作用——就是那些在我们自己测试的时候不希望的功能。

比如,可能我们正在写一个社交软件并且想测试一下“发布到Facebook的功能”,但是我们不希望每次运行测试集的时候都发布到Facebook上。

Python的unittest库中有一个子包叫unittest.mock——或者你把它声明成一个依赖,简化为mock——这个模块提供了非常强大并且有用的方法,通过它们可以模拟或者屏敝掉这些不受我们希望的方面。

pythonmock

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

恐惧系统调用

再举一个例子,考虑系统调用,我们将在余下的文章中讨论它们。不难发现,这些都可以考虑使用模拟:无论你是想写一个脚本弹出一个CD驱动,或者是一个web服务用来删除/tmp目录下的缓存文件,或者是一个socket服务来绑定一个TCP端口,这些调用都是在你单元测试的时候是不被希望的方面。

作为一个开发人员,你更关心你的库是不是成功的调用了系统函数来弹出CD,而不是体验每次测试的时候CD托盘都打开。

作为一个开发人员,你更关心你的库是不是成功调用了系统函数来弹出CD(带着正确的参数等)。而不是体验每次测试的时候CD托盘都打开(或者更糟,很多次,当一个单元测试运行的时候,很多测试点都涉及到了弹出代码)。

同样地,保持你的单元测试效率和性能意味着要还要保留一些自动化测试之外的“缓慢代码”,比如文件系统和网络的访问。

对于我们的第一个例子,我们要重构一个从原始到使用mock的一个标准Python测试用例。我们将会证明如何用mock写一个测试用例使我们的测试更智能、更快,并且能暴露更多关于我们的软件工作的问题。

一个简单的删除功能

有时,我们需要从文件系统中删除文件,因此,我们可以写这样的一个函数在Python中,这个函数将使它更容易成为我们的脚本去完成这件事情。

1234567 #!/usr/bin/env python# -*- coding: utf-8 -*- import os def rm(filename):    os.remove(filename)

很明显,在这个时间点上,我们的rm方法不提供比基本os.remove方法更多的功能,但我们的代码将会有所改进,允许我们在这里添加更多的功能。

让我们写一个传统的测试用例,即,不用模拟测试:

12345678910111213141516171819202122 #!/usr/bin/env python# -*- coding: utf-8 -*- from mymodule import rm import os.pathimport tempfileimport 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重构我们的测试用例:

123456789101112131415 #!/usr/bin/env python# -*- coding: utf-8 -*- from mymodule import rm import mockimport 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模拟对象,并注入到我们测试案例的模拟方法。是模拟os更有意义,还是它在mymodule.os的参考更有意义?

当然,当Python出现在进口和管理模块时,用法是非常的灵活。在运行时,该mymodule模块有自己的os操作系统——被引入到自己的范围内的模块。因此,如果我们模拟os系统,我们不会看到模拟测试在mymodule模块的影响。

这句话需要深刻的记住:

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

如果你需要为myproject.app.MyElaborateClass模拟tempfile模型,你可能需要去模拟myproject.app.tempfile的每个模块来保持自己的进口。

这就是用陷阱的方式来模拟测试。

向‘rm’中加入验证

之前定义的 rm 方法相当的简单 . 在盲目的删除之前,我们会拿它来验证一个路径是否存在,并验证其是否是一个文件. 让我们重构 rm 使其变得更加聪明:

123456789 #!/usr/bin/env python# -*- coding: utf-8 -*- import osimport os.path def rm(filename):    if os.path.isfile(filename):        os.remove(filename)

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

123456789101112131415161718192021222324252627 #!/usr/bin/env python# -*- coding: utf-8 -*- from mymodule import rm import mockimport 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\”)

我们的测试范例完全变化了. 现在我们可以核实并验证方法的内部功能是否有任何副作用.

将删除功能作为服务

到目前为止,我们只是对函数功能提供模拟测试,并没对需要传递参数的对象和实例的方法进行模拟测试。接下来我们将介绍如何对对象的方法进行模拟测试。

首先,我们先将rm方法重构成一个服务类。实际上将这样一个简单的函数转换成一个对象并不需要做太多的调整,但它能够帮助我们了解mock的关键概念。下面是重构的代码:

123456789101112 #!/usr/bin/env python# -*- coding: utf-8 -*- import osimport os.path class RemovalService(object):    \”\”\”A service for removing objects from the filesystem.\”\”\”     def rm(filename):        if os.path.isfile(filename):            os.remove(filename)

你可以发现我们的测试用例实际上没有做太多的改变:

123456789101112131415161718192021222324252627282930 #!/usr/bin/env python# -*- coding: utf-8 -*- from mymodule import RemovalService import mockimport 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如同我们计划的一样工作。接下来让我们创建另一个以该对象为依赖项的服务:

1234567891011

相关内容

热门资讯

500 行 Python 代码... 语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理。自然语言引入了很多意外的歧义,以我们...
定时清理删除C:\Progra... C:\Program Files (x86)下面很多scoped_dir开头的文件夹 写个批处理 定...
65536是2的几次方 计算2... 65536是2的16次方:65536=2⁶ 65536是256的2次方:65536=256 6553...
Mobi、epub格式电子书如... 在wps里全局设置里有一个文件关联,打开,勾选电子书文件选项就可以了。
scoped_dir32_70... 一台虚拟机C盘总是莫名奇妙的空间用完,导致很多软件没法再运行。经过仔细检查发现是C:\Program...
小程序支付时提示:appid和... [Q]小程序支付时提示:appid和mch_id不匹配 [A]小程序和微信支付没有进行关联,访问“小...
pycparser 是一个用... `pycparser` 是一个用 Python 编写的 C 语言解析器。它可以用来解析 C 代码并构...
微信小程序使用slider实现... 众所周知哈,微信小程序里面的音频播放是没有进度条的,但最近有个项目呢,客户要求音频要有进度条控制,所...
python查找阿姆斯特朗数 题目解释 如果一个n位正整数等于其各位数字的n次方之和,则称该数为阿姆斯特朗数。 例如1^3 + 5...
Apache Doris 2.... 亲爱的社区小伙伴们,我们很高兴地向大家宣布,Apache Doris 2.0.0 版本已于...