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

利用Python中的mock库对Python代码进行模拟测试

这篇文章主要介绍了利用Python中的mock库对Python代码进行模拟测试,mock库自从Python3.3依赖成为了Python的内置库,本文也等于介绍了该库的用法,需要的朋友可以参考下

 如何不靠耐心测试

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

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

Python的unittest库中有一个子包叫unittest.mock——或者你把它声明成一个依赖,简化为mock——这个模块提供了非常强大并且有用的方法,通过它们可以模拟或者屏敝掉这些不受我们希望的方面。
 注意:mock是最近收录在Python 3.3标准库中的;之前发布的版本必须通过 PyPI下载Mock库。

恐惧系统调用

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

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

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

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

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

一个简单的删除功能

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

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

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

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

#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import rmimport os.pathimport tempfileimport unittestclass 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.tempfile), "Failed to remove the file.")

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

重构与模拟测试

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

#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import rmimport mockimport 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 parameters
    mock_os.remove.assert_called_with("any path")

对于这些重构,我们已经从根本上改变了该测试的运行方式。现在,我们有一个内部的对象,让我们可以使用另一个功能验证。
潜在的陷阱

第一件要注意的事情就是,我们使用的mock.patch方法的装饰位于mymodule.os模拟对象,并注入到我们测试案例的模拟方法。是模拟os更有意义,还是它在mymodule.os的参考更有意义?

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

这句话需要深刻的记住:

   

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

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

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

向‘rm'中加入验证

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

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

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

#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import rmimport mockimport unittestclass 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的关键概念。下面是重构的代码:
 

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

#!/usr/bin/env python# -*- coding: utf-8 -*-import osimport 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, UploadServiceimport mockimport unittestclass 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")

太棒了!我们验证了上传服务成功调用了实例的rm方法。你是不是注意到这当中有意思的地方了?这种修补机制实际上取代了我们的测试方法的删除服务实例的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的补丁在最外层,因此会在最后被执行,使得它成为实际测试方法的最后一个参数。请特别注意这一点,并且在做测试使用调试器来保证正确的参数按照正确的顺序被注入。

选项2: 创建模拟测试接口

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

#!/usr/bin/env python# -*- coding: utf-8 -*-from mymodule import RemovalService, UploadServiceimport mockimport unittestclass 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")

在这个例子中,我们甚至不需要补充任何功能,只需创建一个带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 facebookimport simple_facebookimport mockimport 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!")    # verify
    mock_put_object.assert_called_with(message="Hello World!")

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

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


推荐阅读
  • 延迟注入工具(python)的SQL脚本
    本文介绍了一个延迟注入工具(python)的SQL脚本,包括使用urllib2、time、socket、threading、requests等模块实现延迟注入的方法。该工具可以通过构造特定的URL来进行注入测试,并通过延迟时间来判断注入是否成功。 ... [详细]
  • 本文介绍了使用postman进行接口测试的方法,以测试用户管理模块为例。首先需要下载并安装postman,然后创建基本的请求并填写用户名密码进行登录测试。接下来可以进行用户查询和新增的测试。在新增时,可以进行异常测试,包括用户名超长和输入特殊字符的情况。通过测试发现后台没有对参数长度和特殊字符进行检查和过滤。 ... [详细]
  • 本文介绍了Python版Protobuf的安装和使用方法,包括版本选择、编译配置、示例代码等内容。通过学习本教程,您将了解如何在Python中使用Protobuf进行数据序列化和反序列化操作,以及相关的注意事项和技巧。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 去掉空格的方法——Python工程师招聘标准与实践
    本文介绍了去掉空格的方法,并结合2019独角兽企业招聘Python工程师的标准与实践进行讨论。同时提供了一个转载链接,链接内容为更多相关信息。 ... [详细]
  • 本文介绍了多因子选股模型在实际中的构建步骤,包括风险源分析、因子筛选和体系构建,并进行了模拟实证回测。在风险源分析中,从宏观、行业、公司和特殊因素四个角度分析了影响资产价格的因素。具体包括宏观经济运行和宏经济政策对证券市场的影响,以及行业类型、行业生命周期和行业政策对股票价格的影响。 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • 无损压缩算法专题——LZSS算法实现
    本文介绍了基于无损压缩算法专题的LZSS算法实现。通过Python和C两种语言的代码实现了对任意文件的压缩和解压功能。详细介绍了LZSS算法的原理和实现过程,以及代码中的注释。 ... [详细]
  • Java String与StringBuffer的区别及其应用场景
    本文主要介绍了Java中String和StringBuffer的区别,String是不可变的,而StringBuffer是可变的。StringBuffer在进行字符串处理时不生成新的对象,内存使用上要优于String类。因此,在需要频繁对字符串进行修改的情况下,使用StringBuffer更加适合。同时,文章还介绍了String和StringBuffer的应用场景。 ... [详细]
  • Webmin远程命令执行漏洞复现及防护方法
    本文介绍了Webmin远程命令执行漏洞CVE-2019-15107的漏洞详情和复现方法,同时提供了防护方法。漏洞存在于Webmin的找回密码页面中,攻击者无需权限即可注入命令并执行任意系统命令。文章还提供了相关参考链接和搭建靶场的步骤。此外,还指出了参考链接中的数据包不准确的问题,并解释了漏洞触发的条件。最后,给出了防护方法以避免受到该漏洞的攻击。 ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
author-avatar
洛熙难耐_467
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有