如何不靠耐心测试

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

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

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

恐惧系统调用

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

作为一个开发人员,你更关心你的库是不是成功的调用了系统函数来弹出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)

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

  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.  
  9. tmpfilepath = os.path.join(tempfile.gettempdir(), "tmp-testfile")
  10.  
  11. def setUp(self):
  12. with open(self.tmpfilepath, "wb") as f:
  13. f.write("Delete me!")
  14. def test_rm(self):
  15. # remove the file
  16. rm(self.tmpfilepath) # test that it was actually removed
  17. self.assertFalse(os.path.isfile(self.tempfile), "Failed to remove the file.")

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

重构与模拟测试

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

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

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

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

向‘rm’中加入验证

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

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

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

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

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

将删除功能作为服务

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

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

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

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

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

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

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

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

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

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

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

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

选项1: 模拟实例的方法

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

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

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

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

陷阱:装饰的顺序

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

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

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

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

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

我们像下面这样使用mock.Mock实例来做测试:

  1. class MethodTestCase(unittest.TestCase):
  2.  
  3. def test_method(self):
  4. target = mock.Mock()
  5. method(target, "value")
  6. 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.  
  3. class SimpleFacebook(object):
  4.  
  5. def __init__(self, oauth_token):
  6. self.graph = facebook.GraphAPI(oauth_token)
  7.  
  8. def post_message(self, message):
  9. """Posts a message to the Facebook wall."""
  10. self.graph.put_object("me", "feed", message=message)

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

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

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

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

  1. from mymodule import test
  2.  
  3. class TestLogParse(unittest2.TestCase):
  4.  
  5. @patch('__builtin__.open')
  6. def test_parse1(self,mock_open):
  7. mock_open.return_value = 'local'
  8. print open('abf')
  9.  
  10. @patch('mymodule.open',create=True)
  11. def test_parse2(self,mock_open):
  12. mock_open.return_value = 'remote'
  13. test()

mock中side_effect的使用

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

  1. @patch('mymodule.open',create=True)
  2. def test_parse(self,mock_open):
  3. def open_side_effect(*args, **kwargs):
  4. if len(args) == 1:
  5. return read_file
  6. else:
  7. return write_file
  8. read_file = StringIO.StringIO()
  9. write_file = StringIO.StringIO()

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

总结

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

Python 的mock模拟测试介绍的更多相关文章

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

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

  2. 【转】利用Python中的mock库对Python代码进行模拟测试

    出处 https://www.toptal.com/python/an-introduction-to-mocking-in-python http://www.oschina.net/transla ...

  3. Mock 模拟测试简介及 Mockito 使用入门

    Mock 是什么mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法.这个虚拟的对象就是mock对象.mock对象就是真实对象在调试期间的代 ...

  4. springboot2.0入门(四)----mock模拟测试+单元测试

    一.本节主要记录模拟测试.单元测试: 二.mock 测试 1.1什么是Mock? 在面向对象程序设计中,模拟对象(英语:mock object,也译作模仿对象)是以可控的方式模拟真实对象行为的假的对象 ...

  5. Python常用的库简单介绍一下

    Python常用的库简单介绍一下fuzzywuzzy ,字符串模糊匹配. esmre ,正则表达式的加速器. colorama 主要用来给文本添加各种颜色,并且非常简单易用. Prettytable ...

  6. 安装nginx python uwsgi环境 以及模拟测试

    uwsgi帮助文档: http://uwsgi-docs-cn.readthedocs.io/zh_CN/latest/WSGIquickstart.html http://uwsgi-docs.re ...

  7. 测试开发Python培训:模拟登录新浪微博-技术篇

    测试开发Python培训:模拟登录新浪微博-技术篇   一般一个初学者项目的起点就是登陆功能的自动化,而面临的项目不同实现的技术难度是不一样的,poptest在做测试开发培训中更加关注技术难点,掌握技 ...

  8. [原创]用Charles模拟App各种网络带宽测试介绍

    [原创]用Charles模拟App各种网络带宽测试介绍 相信每个测试在进行自己公司App测试时,都会碰到一个问题,如何去模拟各种App在各种带宽下的测试情况,估计很少有公司直接去采用2g/3g/4g卡 ...

  9. python之mock模块基本使用

    mock简介 mock原来是python的第三方库 python3以后mock模块已经整合到了unittest测试框架中,不用再单独安装 Mock这个词在英语中有模拟的这个意思,因此我们可以猜测出这个 ...

随机推荐

  1. SurfaceView基本使用--动态画正弦函数

    package com.zzw.TestSurfaceView; import android.content.Context; import android.graphics.Canvas; imp ...

  2. Sqlite/ FMDB

    Sqlite 1. Sqlite数据库 > 数据库? 按数据结构来组织,存储和管理数据的仓库. > 关系型数据库:使用二维表及其之间的联系组织成一个数据组织. 关系:可以理解为一张二维表, ...

  3. IOS对存放对象的数组排序

    我们开发的每个程序都会使用到一些数据,而这些数据一般被封装在一个自定义的类中.例如一个音乐程序可能会有一个Song类,聊天程序则又一个 Friend类,点菜程序会有一个Recipe类等.有时候我们希望 ...

  4. for-in和for 循环 的区别

    以前早就知道,for...in 语句用于对数组或者对象的属性进行循环操作,而for循环是对数组的元素进行循环,而不能引用于非数组对象, 但咱在js项目里,遇到循环,不管是数组还是对象,经常使用for- ...

  5. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space(Java堆空间内存溢出)解决方法

    http://hi.baidu.com/619195553dream/blog/item/be9f12adc1b5a3e71f17a2e9.html问题描述Exception in thread &q ...

  6. LeNet-5网络结构及训练参数计算

    经典神经网络诞生记: 1.LeNet,1998年 2.AlexNet,2012年 3.ZF-net,2013年 4.GoogleNet,2014年 5.VGG,2014年 6.ResNet,201 ...

  7. 演示使用Metasploit入侵Windows

    我使用Kali Linux的IP地址是192.168.0.112:在同一局域网内有一台运行Windows XP(192.168.0.108)的测试电脑. 本文演示怎么使用Metasploit入侵win ...

  8. ORM版,学生管理系统02

    学生管理系统 urls.py url(r'^student_list/$',views.student_list,name="student_list"), url(r'^dele ...

  9. 机器学习(九)—FP-growth算法

    本来老师是想让我学Hadoop的,也装了Ubuntu,配置了Hadoop,一时间却不知从何学起,加之自己还是想先看点自己喜欢的算法,学习Hadoop也就暂且搁置了,不过还是想问一下园子里的朋友有什么学 ...

  10. kali视频(26-30)学习

    第七周 kali视频(26-30)学习 26.KaliSecurity漏洞利用之检索与利用 27.KaliSecurity漏洞利用之Metasploit基础 28.KaliSecurity漏洞利用之M ...