作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
纳夫图里·凯的头像

Naftuli Kay

从构建自定义TCP服务器到大型金融应用程序, Naftuli丰富的经验使他成为一流的开发人员和系统管理员.

Previously At

Splunk
Share

如何在Python中运行单元测试而不测试你的耐心

通常, 我们编写的软件直接与我们称之为“脏”的服务交互. 用外行人的话来说就是:对我们的应用程序至关重要的服务, 但是它们的相互作用会产生意想不到的副作用, 在自主测试运行的上下文中不需要.

例如:也许我们正在编写一款社交应用,想要测试我们的新“发布到Facebook”功能。, but don’t want to actually 每次我们运行我们的测试套件时都会在Facebook上发布.

The Python unittest 库包含一个名为 unittest.mock-或者如果您将其声明为依赖项,则只需 mock-它提供了非常强大和有用的方法来模拟和消除这些不希望出现的副作用.

python unittest模拟库中的模拟和单元测试

Note: unittest.mock is newly included 在Python 3的标准库中.3; prior distributions will have to use the Mock library downloadable via PyPI.

System Calls vs. Python Mocking

为了给您提供另一个示例,我们将在本文的其余部分中使用这个示例,请考虑 system calls. 不难看出,这些都是嘲弄的主要对象:无论您是在编写一个脚本来弹出CD驱动器, 删除过时缓存文件的web服务器 /tmp, 或绑定到TCP端口的套接字服务器, 在单元测试的上下文中,这些调用都具有不希望看到的副作用.

As a developer, 您更关心的是库是否成功调用了弹出CD的系统函数,而不是每次运行测试时都打开CD托盘.

As a developer, 您更关心的是库是否成功调用了用于弹出CD的系统函数(带有正确的参数), etc.),而不是每次运行测试时都实际看到CD托盘打开. (或者更糟糕的是,多次,因为多个测试在单个单元测试运行期间引用弹出代码!)

Likewise, 保持单元测试的效率和性能意味着将尽可能多的“慢代码”排除在自动化测试运行之外, 即文件系统和 network access.

对于我们的第一个示例,我们将把一个标准Python测试用例从原始形式重构为使用 mock. 我们将演示使用mock编写测试用例如何使我们的测试更智能, faster, 并且能够揭示更多关于软件如何工作的信息.

一个简单的删除函数

我们都需要不时地从文件系统中删除文件, 所以让我们用Python写一个函数,这将使我们的脚本更容易完成.

#!/usr/bin/env python
# -*-编码:utf-8 -*-

import os

def rm(文件名):
    os.remove(filename)

Obviously, our rm 方法在此时提供的只是底层的 os.remove 方法,但我们的代码库将得到改进,允许我们在这里添加更多功能.

让我们编写一个传统的测试用例,i.e., without mocks:

#!/usr/bin/env python
# -*-编码:utf-8 -*-

从mymodule导入rm

import os.path
import tempfile
import unittest

类RmTestCase (unittest.TestCase):

    tmpfilepath = os.path.join(tempfile.gettempdir(),“tmp-testfile”)

    def setUp(self):
        with open(self.Tmpfilepath, "wb")作为f:
            f.write("Delete me!")
        
    def test_rm(自我):
        # remove the file
        rm(self.tmpfilepath)
        #测试它是否真的被删除了
        self.assertFalse(os.path.isfile(self.tmpfilepath), "删除文件失败。.")

我们的测试用例非常简单, 但每次它都运行, 创建一个临时文件,然后删除. 另外,我们没有办法测试我们的 rm 方法正确地将参数传递给 os.remove call. We can assume 这是基于上面的测试,但还有很多需要改进的地方.

用Python mock重构

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

#!/usr/bin/env python
# -*-编码:utf-8 -*-

从mymodule导入rm

import mock
import unittest

类RmTestCase (unittest.TestCase):
    
    @mock.patch('mymodule.os')
    Def test_rm(self, mock_os):
        rm("any path")
        测试rm是否称为OS.使用正确的参数删除
        mock_os.remove.assert_called_with(“任何路径”)

通过这些重构,我们已经从根本上改变了测试操作的方式. Now, we have an insider我们可以用一个对象来验证另一个对象的功能.

潜在的Python mock陷阱

首先要注意的是我们使用了 mock.patch 方法装饰器来模拟位于的对象 mymodule.os,并将mock注入到我们的测试用例方法中. 嘲笑不是更有意义吗 os 本身,而不是对它的参考 mymodule.os?

在导入和管理模块方面,Python有点像一条狡猾的蛇. At runtime, the mymodule 模块有自己的 os 哪个在模块中导入到自己的局部作用域. Thus, if we mock os,我们不会看到模拟的效果 mymodule module.

要不断重复的咒语是:

模仿一件物品的使用地点,而不是它的来源.

如果你需要嘲笑 tempfile module for myproject.app.MyElaborateClass,您可能需要将模拟应用于 myproject.app.tempfile,因为每个模块都有自己的导入.

排除了这个陷阱,让我们继续嘲笑吧.

向' rm '添加验证

The rm 方法的定义过于简化了. 我们希望在盲目地尝试删除路径之前验证路径是否存在并且是一个文件. Let’s refactor rm 更聪明一点:

#!/usr/bin/env python
# -*-编码:utf-8 -*-

import os
import os.path

def rm(文件名):
    if os.path.isfile(文件名):
        os.remove(filename)

Great. 现在,让我们调整我们的测试用例来保持覆盖.

#!/usr/bin/env python
# -*-编码:utf-8 -*-

从mymodule导入rm

import mock
import unittest

类RmTestCase (unittest.TestCase):
    
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    Def test_rm(self, mock_os, mock_path):
        #设置模拟
        mock_path.isfile.return_value = False
        
        rm("any path")
        
        #测试remove调用是否被调用.
        self.assertFalse (mock_os.remove.调用"如果文件不存在,无法删除文件".")
        
        #使文件“存在”
        mock_path.isfile.return_value = True
        
        rm("any path")
        
        mock_os.remove.assert_called_with(“任何路径”)

我们的测试范例已经完全改变了. 我们现在可以验证和验证方法的内部功能 any side-effects.

使用Python的Mock Patch的文件删除即服务

So far, 我们一直在为函数提供模型, 但不适用于对象上的方法或发送参数时需要mock的情况. 让我们先介绍一下对象方法.

的重构开始 rm 方法放入服务类中. 确实没有正当的需要, per se, 将这样一个简单的函数封装到一个对象中, 但它至少会帮助我们演示关键概念 mock. Let’s refactor:

#!/usr/bin/env python
# -*-编码:utf-8 -*-

import os
import os.path

类RemovalService(对象):
    """从文件系统中删除对象的服务."""

    def rm(文件名):
        if os.path.isfile(文件名):
            os.remove(filename)

您会注意到在我们的测试用例中没有多少变化:

#!/usr/bin/env python
# -*-编码:utf-8 -*-

从mymodule中导入RemovalService

import mock
import unittest

类RemovalServiceTestCase (unittest.TestCase):
    
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    Def test_rm(self, mock_os, mock_path):
        #实例化我们的服务
        reference = RemovalService()
        
        #设置模拟
        mock_path.isfile.return_value = False
        
        reference.rm("any path")
        
        #测试remove调用是否被调用.
        self.assertFalse (mock_os.remove.调用"如果文件不存在,无法删除文件".")
        
        #使文件“存在”
        mock_path.isfile.return_value = True
        
        reference.rm("any path")
        
        mock_os.remove.assert_called_with(“任何路径”)

很好,现在我们知道 RemovalService works as planned. 让我们创建另一个服务,将其声明为依赖项:

#!/usr/bin/env python
# -*-编码:utf-8 -*-

import os
import os.path

类RemovalService(对象):
    """从文件系统中删除对象的服务."""

    Def (self, filename):
        if os.path.isfile(文件名):
            os.remove(filename)
            

类UploadService(对象):

    Def __init__(self, removal_service):
        self.Removal_service = Removal_service
        
    Def upload_complete(self, filename):
        self.removal_service.rm(filename)

因为我们已经有了测试覆盖 RemovalService,我们不会验证的内部功能 rm 的方法 UploadService. 相反,我们将简单地测试(当然没有副作用) UploadService calls the RemovalService.rm 方法,我们从前面的测试用例中知道它“刚好工作™”.

有两种方法:

  1. Mock out the RemovalService.rm method itself.
  2. 的构造函数中提供模拟实例 UploadService.

由于这两种方法在单元测试中都很重要,我们将对它们进行回顾.

选项1:模拟实例方法

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

#!/usr/bin/env python
# -*-编码:utf-8 -*-

从mymodule中导入RemovalService、UploadService

import mock
import unittest

类RemovalServiceTestCase (unittest.TestCase):
    
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    Def test_rm(self, mock_os, mock_path):
        #实例化我们的服务
        reference = RemovalService()
        
        #设置模拟
        mock_path.isfile.return_value = False
        
        reference.rm("any path")
        
        #测试remove调用是否被调用.
        self.assertFalse (mock_os.remove.调用"如果文件不存在,无法删除文件".")
        
        #使文件“存在”
        mock_path.isfile.return_value = True
        
        reference.rm("any path")
        
        mock_os.remove.assert_called_with(“任何路径”)
      
      
类UploadServiceTestCase (unittest.TestCase):

    @mock.patch.对象(RemovalService,“rm”)
    Def test_upload_complete(self, mock_rm):
        #构建我们的依赖
        removal_service = RemovalService()
        reference = UploadService(removal_service)
        
        #调用upload_complete,然后调用' rm ':
        reference.Upload_complete(“我上传的文件”)
        
        检查它是否调用了RemovalService的rm方法
        mock_rm.Assert_called_with("我上传的文件")
        
        检查它是否调用了_our_ removal_service的rm方法
        removal_service.rm.Assert_called_with("我上传的文件")

Great! 我们已经证实了 UploadService 成功调用我们的实例 rm method. 注意这里有什么有趣的? 补丁机制实际上取代了 rm method of all RemovalService 我们的测试方法中的实例. 这意味着我们实际上可以检查实例本身. 如果你想看更多, 尝试在mock代码中添加一个断点,以更好地了解补丁机制是如何工作的.

模拟补丁陷阱:装饰命令

当在测试方法中使用多个装饰器时, 秩序很重要,这有点令人困惑. 基本上,当将装饰器映射到方法参数时, work backwards. 考虑一下这个例子:

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

自Python补丁到 sys 是最外层的补丁吗, 它将最后执行, 使其成为实际测试方法参数中的最后一个参数. 请注意这一点,并在运行测试时使用调试器,以确保以正确的顺序注入正确的参数.

选项2:创建模拟实例

而不是模拟特定的实例方法,我们可以直接提供一个模拟实例 UploadService 用它的构造函数. 我更喜欢上面的选项1, 因为它更精确, 但在许多情况下,选项2可能是有效的或必要的. 让我们再次重构我们的测试:

#!/usr/bin/env python
# -*-编码:utf-8 -*-

从mymodule中导入RemovalService、UploadService

import mock
import unittest

类RemovalServiceTestCase (unittest.TestCase):
    
    @mock.patch('mymodule.os.path')
    @mock.patch('mymodule.os')
    Def test_rm(self, mock_os, mock_path):
        #实例化我们的服务
        reference = RemovalService()
        
        #设置模拟
        mock_path.isfile.return_value = False
        
        reference.rm("any path")
        
        #测试remove调用是否被调用.
        self.assertFalse (mock_os.remove.调用"如果文件不存在,无法删除文件".")
        
        #使文件“存在”
        mock_path.isfile.return_value = True
        
        reference.rm("any path")
        
        mock_os.remove.assert_called_with(“任何路径”)
      
      
类UploadServiceTestCase (unittest.TestCase):

    Def test_upload_complete(self, mock_rm):
        #构建我们的依赖
        Mock_removal_service = mock.create_autospec (RemovalService)
        reference = UploadService(mock_removal_service)
        
        #调用upload_complete,然后调用' rm ':
        reference.Upload_complete(“我上传的文件”)
        
        #测试是否调用了rm方法
        mock_removal_service.rm.Assert_called_with("我上传的文件")

In this example, 我们甚至不需要修补任何功能, 的自动规范 RemovalService 类,然后将该实例注入到 UploadService 为了验证功能.

The mock.create_autospec 方法创建与所提供的类在功能上等效的实例. What this means, 实际上, 是在与返回的实例交互时吗, 如果以非法方式使用,它将引发异常. More specifically, 如果使用错误数量的参数调用方法, 将引发一个异常. 当重构发生时,这一点非常重要. 随着库的变化,测试会中断,这是预料之中的. 不使用自动规范, 即使底层实现被破坏,我们的测试仍然会通过.

Pitfall: The mock.Mock and mock.MagicMock Classes

The mock 库还包括两个重要的类,它们是构建大多数内部功能的基础: the Python Mock class and the Python MagicMock class. 当让你选择使用 mock.Mock instance, a mock.MagicMock instance, or an auto-spec, 总是倾向于使用自动规格, 因为它有助于保持您的测试对未来的更改保持理智. This is because mock.Mock and mock.MagicMock 不管底层API如何,都接受所有方法调用和属性赋值. 考虑以下用例:

类目标(对象):
    def apply(value):
        return value

Def (target, value):
    return target.apply(value)

我们可以用a来验证 mock.Mock 像这样的例子:

类MethodTestCase (unittest.TestCase):

    def test_method(自我):
        target = mock.Mock()

        方法(目标,“价值”)

        target.apply.assert_called_with(“价值”)

这个逻辑看起来很合理,但是让我们修改一下 Target.apply 方法取更多参数:

类目标(对象):
    Def apply(value, are_you_sure):
        if are_you_sure:
            return value
        else:
            return None

重新运行您的测试,您会发现它仍然通过. 这是因为它不是针对您的实际API构建的. 这就是为什么你应该这么做 always use the create_autospec method and the autospec 参数。 @patch and @patch.object decorators.

Python模拟示例:一个Facebook API调用

To finish up, 让我们编写一个更适用的实际python模拟示例, 我们在介绍中提到过:在Facebook上发布消息. 我们将编写一个很好的包装器类和相应的测试用例.

import facebook

类SimpleFacebook(对象):
    
    Def __init__(self, oauth_token):
        self.graph = facebook.GraphAPI (oauth_token)

    Def post_message(self, message):
        """在Facebook墙上发消息."""
        self.graph.Put_object ("me", "feed", message=message)

下面是我们的测试用例,它检查我们没有发布消息 actually 发布消息:

import facebook
进口simple_facebook
import mock
import unittest

类SimpleFacebookTestCase (unittest.TestCase):
    
    @mock.patch.object(facebook.graphhapi, 'put_object', autospec=True)
    Def test_post_message(self, mock_put_object):
        Sf = simple_facebook.SimpleFacebook(“假的oauth令牌”)
        sf.post_message(“Hello World!")

        # verify
        mock_put_object.assert_called_with(消息= " Hello World!")

正如我们目前所看到的,它是 really 很容易开始编写更智能的测试 mock in Python.

Conclusion

Python’s mock 库,如果使用起来有点混乱,是一个游戏规则改变者 unit-testing. 我们已经演示了开始使用的常见用例 mock 在单元测试中,希望这篇文章能有所帮助 Python developers 克服最初的障碍,写出优秀的、经过测试的代码.

了解基本知识

  • 什么是单元测试中的模拟?

    模拟真实物体的存在和行为, 允许软件工程师在各种假设的场景中测试代码,而不需要诉诸无数的系统调用. 因此,模拟可以极大地提高单元测试的速度和效率.

  • Python中的mock是什么?

    mock在Python中意味着单元测试.模拟库被用来用模拟对象替换系统的某些部分, 允许比其他方式更容易和更有效的单元测试.

聘请Toptal这方面的专家.
Hire Now
纳夫图里·凯的头像
Naftuli Kay

Located in 洛杉矶,加州,美国

Member since April 4, 2016

About the author

从构建自定义TCP服务器到大型金融应用程序, Naftuli丰富的经验使他成为一流的开发人员和系统管理员.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Splunk

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.