一、Mock在单元测试中扮演一个什么角色

  有时,你需要为单元测试的初始设置准备一些“其他”的代码资源。但这些资源兴许会不可用,不稳定,或者是使用起来太笨重。你可以试着找一些其他的资源替代;或者你可以通过创建一个被称为mock的东西来模拟它。Mocks能够让我们模拟那些在单元测试中不可用或太笨重的资源。
  
   在Python中创建mock是通过Mock模块完成的。你可以通过每次一个属性(one-attribute-at-a-time)或一个健全的字典对象或是一个类接口来创建mock。你还可以定义mock的行为并且在测试过程中检查它的使用。让我们继续探讨。

二、测试准备

Test Case           Test Subject         Test Resource

  典型的测试准备最少有两个部分。首先是测试对象(红色),这是测试的关注点。它可以是一个方法、模块或者类。它可以返回一个结果,也可以不返回结果,但是它可以根据数据数据或者内部状态产生错误或者异常。
  
  第二测试用例(灰色),它可以单独运行也可以作为套件的一部分。它是为测试对象准备的,也可以是测试对象需要的任意数据或资源。运行一个或多个测试事务,在每个测试中检查测试对象的行为。收集测试结果并用一个简洁、易读的格式呈现测试结果。
  
  现在,为了发挥作用,一些测试对象需要一个或多个资源(绿色)。这些资源可以是其他的类或者模块,甚至是一个非独立的进程。不论其性质,测试资源是功能性的代码。他们的角色是支持测试对象,但是他们不是测试的关注点。

三、使用Mock的理由

  但是有些时候,测试资源不可用,或者不适合。也许这个资源正在和测试对象并行开发中,或者并不完整或者是太不稳定以至于不可靠。
  
  测试资源太昂贵,如果测试资源是第三方的产品,其高昂的价格不适用于测试。测试资源的建立过于复杂,占用的硬件和时间可以用于别的地方。如果测试资源是一个数据源,建立它的数据集模仿真实世界是乏味的。
  
  测试资源是不可预知的。一个好的单元测试是可重复的,允许你分离和识别故障。但是测试资源可能给出随机的结果,或者它会有不同的响应时间。而作为这样的结果,测试资源最终可能成为一个潜在的搅局者。
  
   这些都是你可能想要用mock代替测试资源的原因。mock向测试对象提供一套和测试资源相同的方法接口。但是mock是更容易创建和管理。它能向测试对象提供和真实的测试资源相同的方法接口。它能提供确定的结果,并可以自定义以适用于特定的测试。能够容易的更新,以反映实际资源的变化。
  
  当然,mocks不是没有问题的。设计一个精确的mock是困难的,特别是如果你没有测试资源的可靠信息。你可以尝试找到一个开源的接口,或者你能对测试资源的方法接口进行猜测。无论你如何选择,你都可以在以后轻松的更新mock,你可以在首选资源中得到更详细的信息。
  
  太多的mock会使测试过于复杂,让你跟踪错误变得更困难。最好的实践是每个测试用例限制使用一到两个mock,或者为每个mock/对象对使用独立的测试用例。

四、使用Python Mock

  在Python中Mock模块是用来创建和管理mock对象的。该模块是Michael Foord的心血结晶,它是Python3.0的标准模块。因此在Python2.4~2.7中,你不得不自己安装这个模块。你可以 Python Package Index website从获得Mock模块最新的版本。
  Mock模块中有两个非常重要的类Mock、MagicMock和一个重要的方法create_autospec。

五、MagicMock类

MagicMock类是Mock类的子类,区别在于MagicMock类实现了常用的魔术方法,比如__str__、__iter__等,其他一样。

六、mock.create_autospce

  mock.create_autospec为类提供了一个同等功能实例。这意味着,实际上来说,在使用返回的实例进行交互的时候,如果使用了非法的方法将会引发异常。更具体地说,如果一个方法被调用时的参数数目不正确,将引发一个异常。这对于重构来说是非常重要。当一个库发生变化的时候,中断测试正是所期望的。如果不使用auto-spec,即使底层的实现已经破坏,我们的测试仍然会通过。
  在选择使用mock.Mock实例,mock.MagicMock实例或create_autospec方法的时候,通常倾向于选择使用 create_autospec方法,因为它能够对未来的变化保持测试的合理性。这是因为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参数。

七、mock.patch和mock.patch.object

1.参数

unittest.mock.patch(target,new = DEFAULT,spec = None,create = False,spec_set = None,autospec = None,new_callable = None,** kwargs )

    • target参数必须是一个str,格式为'package.module.ClassName',
      注意这里的格式一定要写对,如果你的函数或类写在pakege名称为a下,b.py脚本里,有个c的函数(或类),那这个参数就写“a.b.c”

    • new参数如果没写,默认指定的是MagicMock
    • spec=True或spec_set=True,这会导致patch传递给被模拟为spec / spec_set的对象
    • new_callable允许您指定将被调用以创建新对象的不同类或可调用对象。默认情况下MagicMock使用。

注意:@mock.patch整个对象,@mock.patch.object对象中的方法

示例

# 文件名:Mymodel
class MyTest(object):
def func(self):
pass

那么@mock.patch

import Mymodel

class AppMockTests(unit.TestCase):

    def setUp(self):
super(AppMockTests, self).setUp()
self.project_zbj = webtest.TestApp(self.loadapp('project_zbj')) @mock.patch('Mymodel.MyTest') # 必须是字符串,具体到类
def test_list(self, mock_MyTest):
url = '/api/zzz'
mock_MyTest.func.return_value = 500
res = self.project_zbj.get(url)
self.assertEqual(res.json['data'], 500)

而@mock.patch.object

import Mymodel

class AppMockTests(unit.TestCase):

    def setUp(self):
super(AppMockTests, self).setUp()
self.project_zbj = webtest.TestApp(self.loadapp('project_zbj')) @mock.patch.object(Mymodel.MyTest, 'func') # 具体到某个方法
def test_list(self, mock_func):
url = '/api/zzz'
mock_func.return_value = 500
res = self.project_zbj.get(url)
self.assertEqual(res.json['data'], 500)

八、python3中的mock(前面介绍的是python2的)

  Mocks让我们为单元测试模拟了那些不可用或者是太庞大的资源。我们可以在运行中配置mock,在特定的测试中改变它的行为或响应,或者让它在恰当的时候抛出错误和异常。

 但在,实际生产中的项目是非常复杂的,对其进行单元测试的时候,会遇到以下问题:

  • 接口的依赖
  • 外部接口调用
  • 测试环境非常复杂

  单元测试应该只针对当前单元进行测试, 所有的内部或外部的依赖应该是稳定的, 已经在别处进行测试过的.使用mock 就可以对外部依赖组件实现进行模拟并且替换掉, 从而使得单元测试将焦点只放在当前的单元功能。

1、简单的例子

我们先从最简单例子开始。

modular.py

#modular.py

class Count():

    def add(self):
pass

这里要实现一个Count计算类,add() 方法要实现两数相加。但,这个功能我还没有完成。这时就可以借助mock对其进行测试。

mock_demo01.py

from unittest import mock
import unittest from modular import Count # test Count class
class TestCount(unittest.TestCase): def test_add(self):
count = Count()
count.add = mock.Mock(return_value=7)
result = count.add(2,5)
self.assertEqual(result,7)
# 如果是python2,那么return_value的值会保存在返回值的json格式中的data
     self.assertEqual(result.json['data'], 7) if __name__ == '__main__':
unittest.main()

  count = Count()

  首先,调用被测试类Count() 。

  count.add = mock.Mock(return_value=7)

  通过Mock类模拟被调用的方法add()方法,return_value 定义add()方法的返回值。

  result = count.add(2,5)

  接下来,相当于在正常的调用add()方法,传两个参数2和5,然后会得到相加的结果7。然后,7的结果是我们在上一步就预先设定好的。

  self.assertEqual(result,7)

  最后,通过assertEqual()方法断言,返回的结果是否是预期的结果7。

  运行测试结果:

> python3 mock_demo01.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s OK

这样一个用例就在mock的帮助下编写完成,并且测试通过了。

2、完成功能测试

再接下来完成module.py文件中add()方法。

# module.py

class Count():

    def add(self, a, b):
return a + b

然后,修改测试用例:

from unittest import mock
import unittest
from module import Count class MockDemo(unittest.TestCase): def test_add(self):
count = Count()
count.add = mock.Mock(return_value=13, side_effect=count.add)
result = count.add(8, 8)
print(result)
count.add.assert_called_with(8, 8)
self.assertEqual(result, 16) if __name__ == '__main__':
unittest.main()

 count.add = mock.Mock(return_value=13, side_effect=count.add)

  side_effect参数和return_value是相反的。它给mock分配了可替换的结果,覆盖了return_value。简单的说,一个模拟工厂调用将返回side_effect值,而不是return_value。

  所以,设置side_effect参数为Count类add()方法,那么return_value的作用失效。

  result = count.add(8, 8)

  print(result)

  这次将会真正的调用add()方法,得到的返回值为16(8+8)。通过print打印结果。

  assert_called_with(8,8)

  检查mock方法是否获得了正确的参数。

3、解决测试依赖                           

   前面的例子,只为了让大家对mock有个初步的印象。再接来,我们看看如何mock方法的依赖。

  例如,我们要测试A模块,然后A模块依赖于B模块的调用。但是,由于B模块的改变,导致了A模块返回结果的改变,从而使A模块的测试用例失败。其实,对于A模块,以及A模块的用例来说,并没有变化,不应该失败才对。

  这个时候就是mock发挥作用的时候了。通过mock模拟掉影响A模块的部分(B模块)。至于mock掉的部分(B模块)应该由其它用例来测试。

# function.py
def add_and_multiply(x, y):
addition = x + y
multiple = multiply(x, y)
return (addition, multiple) def multiply(x, y):
return x * y

  然后,针对 add_and_multiply()函数编写测试用例。func_test.py

import unittest
import function class MyTestCase(unittest.TestCase): def test_add_and_multiply(self):
x = 3
y = 5
addition, multiple = function.add_and_multiply(x, y)
self.assertEqual(8, addition)
self.assertEqual(15, multiple) if __name__ == "__main__":
unittest.main()

运行结果:

>  python3 func_test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s OK

  

  目前运行一切正确常,然而,add_and_multiply()函数依赖了multiply()函数的返回值。如果这个时候修改multiply()函数的代码。

……
def multiply(x, y):
return x * y + 3

  这个时候,multiply()函数返回的结果变成了x*y加3。

  再次运行测试:

>  python3 func_test.py
F
======================================================================
FAIL: test_add_and_multiply (__main__.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "fun_test.py", line 19, in test_add_and_multiply
self.assertEqual(15, multiple)
AssertionError: 15 != 18 ----------------------------------------------------------------------
Ran 1 test in 0.000s FAILED (failures=1)

  测试用例运行失败了,然而,add_and_multiply()函数以及它的测试用例并没有做任何修改,罪魁祸首是multiply()函数引起的,我们应该把 multiply()函数mock掉。

import unittest
from unittest.mock import patch
import function class MyTestCase(unittest.TestCase): @patch("function.multiply")
def test_add_and_multiply2(self, mock_multiply):
x = 3
y = 5
mock_multiply.return_value = 15
addition, multiple = function.add_and_multiply(x, y)
mock_multiply.assert_called_once_with(3, 5) self.assertEqual(8, addition)
self.assertEqual(15, multiple) if __name__ == "__main__":
unittest.main()

  @patch("function.multiply")

  patch()装饰/上下文管理器可以很容易地模拟类或对象在模块测试。在测试过程中,您指定的对象将被替换为一个模拟(或其他对象),并在测试结束时还原。

  这里模拟function.py文件中multiply()函数。

  def test_add_and_multiply2(self, mock_multiply):

  在定义测试用例中,将mock的multiply()函数(对象)重命名为 mock_multiply对象

  mock_multiply.return_value = 15

  设定mock_multiply对象的返回值为固定的15。

  ock_multiply.assert_called_once_with(3, 5)

  检查ock_multiply方法的参数是否正确。

  再次,运行测试用例,通过!

mock详解的更多相关文章

  1. 单元测试系列之十一:Jmockit之mock特性详解

    本文是Jmockit学习过程中,根据官网所列的工具特性进行解读. 1.调用次数约束(Invocation count constraints) 可以通过调用计数约束来指定预期和/或允许匹配给定期望的调 ...

  2. AJAX请求详解 同步异步 GET和POST

    AJAX请求详解 同步异步 GET和POST 上一篇博文(http://www.cnblogs.com/mengdd/p/4191941.html)介绍了AJAX的概念和基本使用,附有一个小例子,下面 ...

  3. 常见 jar包详解

        常见 jar包详解 jar包 用途 axis.jar SOAP引擎包 commons-discovery-0.2.jar 用来发现.查找和实现可插入式接口,提供一些一般类实例化.单件的生命周期 ...

  4. 提高Java代码质量的Eclipse插件之Checkstyle的使用详解

    提高Java代码质量的Eclipse插件之Checkstyle的使用详解 CheckStyle是SourceForge下的一个项目,提供了一个帮助JAVA开发人员遵守某些编码规范的工具.它能够自动化代 ...

  5. [原创] 分享我们自己搭建的微信小程序开发框架——wframe及设计思想详解

    wframe不是控件库,也不是UI库,她是一个微信小程序面向对象编程框架,代码只有几百行.她的主要功能是规范小程序项目的文件结构.规范应用程序初始化.规范页面加载及授权管理的框架,当然,wframe也 ...

  6. 【微信小程序项目实践总结】30分钟从陌生到熟悉 web app 、native app、hybrid app比较 30分钟ES6从陌生到熟悉 【原创】浅谈内存泄露 HTML5 五子棋 - JS/Canvas 游戏 meta 详解,html5 meta 标签日常设置 C#中回滚TransactionScope的使用方法和原理

    [微信小程序项目实践总结]30分钟从陌生到熟悉 前言 我们之前对小程序做了基本学习: 1. 微信小程序开发07-列表页面怎么做 2. 微信小程序开发06-一个业务页面的完成 3. 微信小程序开发05- ...

  7. 单元测试系列之四:Sonar平台中项目主要指标以及代码坏味道详解

    更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6766994.html 众所周知Sona ...

  8. vue 文件目录结构详解

    vue 文件目录结构详解 本篇文章主要介绍了vue 文件目录结构详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 项目简介 基于 vue.js 的前端开发环境,用于前后 ...

  9. Spring MVC测试框架详解——服务端测试

    随着RESTful Web Service的流行,测试对外的Service是否满足期望也变的必要的.从Spring 3.2开始Spring了Spring Web测试框架,如果版本低于3.2,请使用sp ...

随机推荐

  1. Python编程从入门到实践笔记——操作列表

    Python编程从入门到实践笔记——操作列表 #coding=utf-8 magicians = ['alice','david','carolina'] #遍历整个列表 for magician i ...

  2. Cayley图数据库的简介及使用

    图数据库   在如今数据库群雄逐鹿的时代中,非关系型数据库(NoSQL)已经占据了半壁江山,而图数据库(Graph Database)更是攻城略地,成为其中的佼佼者.   所谓图数据库,它应用图理论( ...

  3. oracle锁表与解表

    查看锁表进程SQL语句1: select sess.sid, sess.serial#, lo.oracle_username, lo.os_user_name, ao.object_name, lo ...

  4. C# 2进制、8进制、10进制、16进制...各种进制间的转换(三) 数值运算和位运算

    一.数值运算 各进制的数值计算很简单,把各进制数转换成 十进制数进行计算,然后再转换成原类型即可. 举例 :二进制之间的加法 /// <summary> /// 二进制之间的加法 /// ...

  5. EF 查询视图出现重复数据

    解决方案: 由多张表组成的视图,要加实体键.而且实体键组合要能确保唯一性. 个人理解:确保唯一性,一个或多个实体键,实现了复合主键或组合主键的效果. 这样查询是,延迟加载机制,才知道哪些需要重新从数据 ...

  6. Java虚拟机垃圾收集算法

    1.标记-清除算法 标记-清除算法分为 "标记" 和 "清除" 两个步骤:首先标记出所有需要回收的对象,然后在标记完成后统一回收所有被标记的对象,是垃圾收集算法 ...

  7. mysql命令行导入导出数据库

    导出:1.在命令行里,进入mysql安装根目录下的bin目录下比如:D:\Program Files\MySQL\MySQL Server 5.0\bin输入 mysqldump -uroot -p ...

  8. Centos7下用FastDFS搭建图片服务器

    1.所用到的工具: 1.FastDFS_v5.05.tar 2.fastdfs-nginx-module_v1.16.tar 3.libfastcommonV1.0.7.tar 4.nginx-1.1 ...

  9. Android应用跳转到微信和支付宝扫一扫

    新版的微信已经把微信功能的schema都禁掉了,意味着我们无法打开微信的扫一扫等功能,目前正常的只能先进入微信(我测试的时候是微信版本7.0.3)已经是很新的版本了 具体调起微信扫一扫代码如下,测试后 ...

  10. Java中的守护线程

    守护线程的概念 在java中有两种线程,守护线程和非守护线程,其两者并没有本质的区别,唯一的区别就是当前的用户线程退出的时候,若只存在唯一的A线程,若A线程为守护线程,那么JVM将会直接退出,否则JV ...