如何不靠耐心测试

可能我们正在写一个社交软件并且想测试一下“发布到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 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 rm
import mock
import 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")

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

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

向‘rm’中加入验证

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

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

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

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

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

将删除功能作为服务

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

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

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

#!/usr/bin/env python#
-*- coding: utf-8 -*-
import os
import os.path class 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 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") 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方法。这意味着,我们实际上可以检查该实例本身。如果你想了解更多,可以试着在模拟测试的代码中下断点来更好的认识这种修补机制是如何工作的。

@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 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") 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.Mockmock.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 facebook

class 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 unittest class 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 开始编写更加聪明的测试是真的很简单的.

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

from mymodule import test

class 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_file
else:
return write_file
read_file = StringIO.StringIO()
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. C++轮子队-第五周--测试与发布

    Alpha版本测试报告 测试找出的BUG 测试结果bug清单: 修复的bug: 按方向下键部分情况无法合并的bug 棋盘图形布局错乱的bug 分数显示不出来的bug 重开游戏无法下坠方块的bug 无法 ...

  2. 解决AndroidStudio导入项目在 Building gradle project info 一直卡住

    Android Studio导入项目的时候,一直卡在Building gradle project info这一步,主要原因还是因为被墙的结果.gradle官网虽然可以访问,但是速度连蜗牛都赶不上.. ...

  3. go语言通过反射获取和设置结构体字段值的方法

    本文实例讲述了go语言通过反射获取和设置结构体字段值的方法.分享给大家供大家参考.具体实现方法如下: type MyStruct struct { N int } n := MyStruct{ 1 } ...

  4. bzoj 3124 直径

    Written with StackEdit. Description 小\(Q\)最近学习了一些图论知识.根据课本,有如下定义. 树:无回路且连通的无向图,每条边都有正整数的权值来表示其长度.如果一 ...

  5. 如何在本地浏览器访问nginx

    1.打开vmware"编辑虚拟机"设置,点击“网络适配器”选择“桥联模式”: 2.开启该虚拟机,输入用户名root及密码登陆服务器: 3.以管理员身份打开cmd,在命令窗口输入ip ...

  6. bzoj 1119 [POI2009]SLO && bzoj 1697 [Usaco2007 Feb]Cow Sorting牛排序——思路(置换)

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1119 https://www.lydsy.com/JudgeOnline/problem.p ...

  7. bzoj 2259 [Oibh]新型计算机 ——最短路(建图)

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=2259 不是 n^2 条边!连那条边权为0的边之后,只要每个位置向它的前一个位置和后一个位置连 ...

  8. Toolbar使用

    原文地址 http://www.cnblogs.com/Dentist/p/4370176.html Android4.0出现的Actionbar提供了同意方便的导航管理.很大程度的统一了Androi ...

  9. gen already exists but is not a source folder

    遇到android项目导入出现后重复空包等错误,往往是导入的java编译级别有关,点击项目properties-> java Compiler ->修改Compiler complianc ...

  10. The method of type must override a superclass method解决方式(转)

    今天做struts2上传文件的时候出现了这个问题: The method execute() of type UploadAction must override or implement a sup ...