热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

测试人必会的Python的mock模拟测试

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

如何不靠耐心测试

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

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

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


恐惧系统调用

无论你是想写一个脚本弹出一个CD驱动,或者是一个web服务用来删除/tmp目录下的缓存文件,或者是一个socket服务来绑定一个TCP端口,这些调用都是在你单元测试的时候是不被希望的方面。

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

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


一个简单的删除功能

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

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

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

#!/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 filerm(self.tmpfilepath) # test that it was actually removedself.assertFalse(os.path.isfile(self.tempfile), "Failed to remove the file.")

当它每次运行时,一个临时文件被创建然后被删除。我们没有办法去测试我们的rm方法是否传递参数到os.remove中。我们可以假设它是基于上面的测试,但仍有许多需要被证实。


重构与模拟测试

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

#!/usr/bin/env python#
-*- coding: utf-8 -*-
from mymodule import rm
import mock
import unittestclass 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 parametersmock_os.remove.assert_called_with("any path")

对于这些重构,我们已经从根本上改变了该测试的运行方式。

现在,mymodule模块中的os对象已经被mock对象替换,当调用mymodule的os模块的remove方法时,实际调用的是mock_os这个mock对象的remove方法。


向‘rm’中加入验证

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

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

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

#!/usr/bin/env python#
-*- coding: utf-8 -*-
from mymodule import rm
import mock
import unittestclass RmTestCase(unittest.TestCase):@mock.patch('mymodule.os.path')@mock.patch('mymodule.os')def test_rm(self, mock_os, mock_path):# set up the mockmock_path.isfile.return_value = Falserm("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 = Truerm("any path")mock_os.remove.assert_called_with("any path")

我们的测试范例完全变化了.mymodule的os模块的isfile方法也被mock对象替换。


将删除功能作为服务

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

首先,我们先将rm方法重构成一个服务类。下面是重构的代码:

#!/usr/bin/env python#
-*- coding: utf-8 -*-
import os
import os.pathclass 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 unittestclass RemovalServiceTestCase(unittest.TestCase):@mock.patch('mymodule.os.path')@mock.patch('mymodule.os')def test_rm(self, mock_os, mock_path):# instantiate our servicereference = RemovalService() # set up the mockmock_path.isfile.return_value = Falsereference.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 = Truereference.rm("any path")mock_os.remove.assert_called_with("any path")

很好,RemovalService如同我们计划的一样工作。接下来让我们创建另一个以该对象为依赖项的服务:

#!/usr/bin/env python#
-*- coding: utf-8 -*-
import os
import os.pathclass RemovalService(object):"""A service for removing objects from the filesystem."""def rm(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(filename):self.removal_service.rm(filename)

到目前为止,我们的测试已经覆盖了RemovalService, 我们不会对我们测试用例中UploadService的内部函数rm进行验证。相反,我们将调用UploadService的RemovalService.rm方法来进行简单的测试(为了不产生其他副作用),我们通过之前的测试用例可以知道它可以正确地工作。

有两种方法可以实现以上需求:


  1. 模拟RemovalService.rm方法本身。

  2. 在UploadService类的构造函数中提供一个模拟实例。

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


选项1: 模拟实例的方法

该模拟库有一个特殊的方法用来装饰模拟对象实例的方法和参数。@mock.patch.object 进行装饰:

#!/usr/bin/env python#
-*- coding: utf-8 -*-
from mymodule import RemovalService, UploadService
import mock
import unittestclass RemovalServiceTestCase(unittest.TestCase):@mock.patch('mymodule.os.path')@mock.patch('mymodule.os')def test_rm(self, mock_os, mock_path):# instantiate our servicereference = RemovalService() # set up the mockmock_path.isfile.return_value = Falsereference.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 = Truereference.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 dependenciesremoval_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 RemovalServicemock_rm.assert_called_with("my uploaded file") # check that it called the rm method of _our_ removal_serviceremoval_service.rm.assert_called_with("my uploaded file")

这种修补机制实际上取代了我们的测试方法的删除服务实例的rm方法。这意味着,我们实际上可以检查该实例本身。如果你想了解更多,可以试着在模拟测试的代码中下断点来更好的认识这种修补机制是如何工作的。

@mock.patch.object用来对一个对象的某个方法或者属性进行替换。


陷阱:装饰的顺序

当使用多个装饰方法来装饰测试方法的时候,装饰的顺序很重要,但很容易混乱。基本上,当装饰方法呗映射到带参数的测试方法中时,装饰方法的工作顺序是反向的。比如下面这个例子:

@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: 创建模拟测试接口

我们可以在UploadService的构造函数中提供一个模拟测试实例,而不是模拟创建具体的模拟测试方法。 我推荐使用选项1的方法,因为它更精确,但在多数情况下,选项2是必要的并且更加有效。让我们再次重构我们的测试实例:

#!/usr/bin/env python#
-*- coding: utf-8 -*-
from mymodule import RemovalService, UploadService
import mock
import unittestclass RemovalServiceTestCase(unittest.TestCase):@mock.patch('mymodule.os.path')@mock.patch('mymodule.os')def test_rm(self, mock_os, mock_path):# instantiate our servicereference = RemovalService() # set up the mockmock_path.isfile.return_value = Falsereference.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 = Truereference.rm("any path")mock_os.remove.assert_called_with("any path") class UploadServiceTestCase(unittest.TestCase):def test_upload_complete(self, mock_rm):# build our dependenciesmock_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 methodmock_removal_service.rm.assert_called_with("my uploaded file")

在这个例子中,我们甚至不需要补充任何功能,只需创建一个带auto-spec方法的RemovalService类,然后将该实例注入到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 valuedef 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 facebookclass 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)

下面是我们的测试用例, 它检查到我发送了信息,但并没有实际的发送出这条信息(到Facebook上):

import facebook
import simple_facebook
import mock
import unittestclass 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!") # verifymock_put_object.assert_called_with(message="Hello World!")

就我们目前所看到的,在Python中用 mock 开始编写更加聪明的测试是真的很简单的.


如何用mock模拟python的builtin内建函数

from mymodule import testclass TestLogParse(unittest2.TestCase):@patch('__builtin__.open')def test_parse1(self,mock_open):mock_open.return_value = 'local'print open('abf')@patch('mymodule.open',create=True)def test_parse2(self,mock_open):mock_open.return_value = 'remote'test()

mock中side_effect的使用

为mock对象指定side_effect属性后,每次mock被调用,side_effect都将被调用,并且调用的参数也会被传递进来。我们可以根据这点来做一些判断。

@patch('mymodule.open',create=True)def test_parse(self,mock_open):def open_side_effect(*args, **kwargs):if len(args) == 1:return read_fileelse:return write_fileread_file = StringIO.StringIO()write_file = StringIO.StringIO()

这里,根据open传递的参数个数来判断返回的对象。
 


总结

Python的 mock 库, 使用起来是有点子迷惑, 是单元测试的游戏规则变革者. 我们通过开始在单元测试中使用 mock ,展示了一些通常的使用场景, 希望这篇文章能帮助 Python 克服一开始的障碍,写出优秀的,能经得起测试的代码.

 


推荐阅读
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 本文介绍了OpenStack的逻辑概念以及其构成简介,包括了软件开源项目、基础设施资源管理平台、三大核心组件等内容。同时还介绍了Horizon(UI模块)等相关信息。 ... [详细]
  • 本文介绍了OkHttp3的基本使用和特性,包括支持HTTP/2、连接池、GZIP压缩、缓存等功能。同时还提到了OkHttp3的适用平台和源码阅读计划。文章还介绍了OkHttp3的请求/响应API的设计和使用方式,包括阻塞式的同步请求和带回调的异步请求。 ... [详细]
  • 本文介绍了使用Spark实现低配版高斯朴素贝叶斯模型的原因和原理。随着数据量的增大,单机上运行高斯朴素贝叶斯模型会变得很慢,因此考虑使用Spark来加速运行。然而,Spark的MLlib并没有实现高斯朴素贝叶斯模型,因此需要自己动手实现。文章还介绍了朴素贝叶斯的原理和公式,并对具有多个特征和类别的模型进行了讨论。最后,作者总结了实现低配版高斯朴素贝叶斯模型的步骤。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 微信开放外链的第二阶段:腾讯和阿里巴巴的博弈
    2021年11月30日,微信开始进行“开放外链”的第二阶段,允许在微信个人会话中打开外部链接和在微信群中打开电商链接。虽然这是腾讯和阿里巴巴都能接受的阶段性结果,但双方都不会太满意。接下来几个月,腾讯和阿里将展开复杂的博弈,我们作为外人很难看清全过程。工信部从未要求腾讯无条件开放微信API,本次开放的也只是普通的HTTP链接。 ... [详细]
  • 树莓派语音控制的配置方法和步骤
    本文介绍了在树莓派上实现语音控制的配置方法和步骤。首先感谢博主Eoman的帮助,文章参考了他的内容。树莓派的配置需要通过sudo raspi-config进行,然后使用Eoman的控制方法,即安装wiringPi库并编写控制引脚的脚本。具体的安装步骤和脚本编写方法在文章中详细介绍。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • 本文介绍了绕过WAF的XSS检测机制的方法,包括确定payload结构、测试和混淆。同时提出了一种构建XSS payload的方法,该payload与安全机制使用的正则表达式不匹配。通过清理用户输入、转义输出、使用文档对象模型(DOM)接收器和源、实施适当的跨域资源共享(CORS)策略和其他安全策略,可以有效阻止XSS漏洞。但是,WAF或自定义过滤器仍然被广泛使用来增加安全性。本文的方法可以绕过这种安全机制,构建与正则表达式不匹配的XSS payload。 ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • 背景应用安全领域,各类攻击长久以来都危害着互联网上的应用,在web应用安全风险中,各类注入、跨站等攻击仍然占据着较前的位置。WAF(Web应用防火墙)正是为防御和阻断这类攻击而存在 ... [详细]
author-avatar
qr筱然陋室
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有