浅析Mock,Fake和Stub在测试中的应用

自动化测试中,我们常会使用一些经过简化的,行为与表现类似于生产环境下的对象的复制品。引入这样的复制品能够降低构建测试用例的复杂度,允许我们独立而解耦地测试某个模块,不再担心受到系统中其他部分的影响。

在《The Art of Unit Testing》书中Mock 被描述为假对象,通过验证是否发生与对象的交互来帮助确定测试是否失败或通过。其他的东西都被定义为Stub。在这本书中,Fake对象就是不真实的,根据它们的使用情况,它们可以是Stub,也可以是Mock。

更复杂一点的定义是Gerard Meszaros 在XunitPatterns中对此类对象的定义。他对这类对象统一称呼为:Test Double。包含:Dummy,Fake,Spy,Mock和Stub。

Test Double种类.png

而通常,测试人员更倾向于使用 Mock 来统一描述不同的 Test Doubles。

不过对于 Test Doubles 实现的误解还是可能会影响到测试的设计,使测试用例变得混乱和脆弱,最终带来不必要的重构。CC先生就最常用的Mock,Fake和Stub来解释一下不同的 Double 的使用场景。

Fake:We use a Fake Object to replace the functionality of a real DOC in a test for reasons other than verification of indirect inputs and outputs of the SUT. Typically, it implements the same functionality as the real DOC but in a much simpler way. While a Fake Object is typically built specifically for testing, it is not used as either a control point or a observation point by the test.
简单的来说,Fake 是那些包含了生产环境下具体实现的简化版本的对象。

比如在测试系统时需要频繁的连接数据库进行操作,而此时有可能数据库还没有完全实现,我们就可以采用快速编写系统原型,并且基于内存存储来运行整个系统,推迟有关数据库设计所用到的一些决定来加速测试环境的搭建。另一个常见的使用场景就是利用 Fake 来保证在测试环境下支付永远返回成功结果。

Stub:Test stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
Stub只是返回一个规定的值,而不会去涉及到系统的任何改变。

比较常见的场景就是系统希望去查询某一类的信息,而Stub可以总是返回一个固定值,比如发送邮件的功能,Stub可以总是返回邮件发送成功的标识1,但是你并不知道你到底发送了邮件给谁或者发送了几封邮件。

Mock:We can use a Mock Object as an observation point that is used to verify the indirect outputs of the SUT as it is exercised. Typically, the Mock Object also includes the functionality of a Test Stub in that it must return values to the SUT if it hasn't already failed the tests but the emphasisis on the verification of the indirect outputs. Therefore, a Mock Object is lot more than just a Test Stub plus assertions; it is used a fundamentally different way.

就算在Gerard Meszaros的定义里面我们可以看出Mock和Stub有一定的重合性,比较大的区别是Mock专注于observation point,而Stub专注于control point,或者从另一个角度上面来说,Mock是会有行为的更改,而Stub只是状态的一个变化而已。


在Python 3.3以前的版本中,需要另外安装mock模块,可以使用pip命令来安装

pip install mock

使用的时候直接导入即可:

import mock

从Python 3.3开始,mock模块已经被合并到标准库中,被命名为unittest.mock,可以直接import进来使用:

from unittest import mock

也就是说我们以后使用Python的时候不用导入任何的第三方包就可以方便使用Mock来模拟测试对象的。Python中的Mock是非常容易使用,可以说是在unittest中使用最多。 模拟是基于“动作 - >断言”模式,而不是许多Mock框架使用的“记录 - >重放”。

Mock的基础使用

Mock对象的一般用法是这样的:

  1. 找到你要替换的对象,这个对象可以是一个类,或者是一个函数,或者是一个类实例。
  2. 实例化Mock类得到一个mock对象,并且设置这个mock对象的行为,比如被调用的时候返回什么值,被访问成员的时候返回什么值等。
  3. 使用这个mock对象替换掉我们想替换的对象,也就是步骤1中确定的对象。

之后就可以开始写测试代码,这个时候我们可以保证我们替换掉的对象在测试用例执行的过程中行为和我们预设的一样。

举个例子: 简单定义一个Person类,其中的代码为:

class Person:
    def __init__(self):
        self.__age = 10
        
    def get_fullname(self, first_name, last_name):
        return first_name + ' ' + last_name
        
    def get_age(self):
        return self.__age
        
    @staticmethod
    def get_class_name():
        return Person.__name__

类里有两个成员方法,一个有参数,一个无参数,还有一个静态方法

1). 使用Mock类,返回固定值
新建一个文件叫MockPerson.py,来测试:

from unittest import mock
import unittest
from .person import Person


class PersonTest(unittest.TestCase):
    def test_should_get_age(self):
        p = Person()

        # 不mock时,get_age应该返回10
        self.assertEqual(p.get_age(), 10)

        # mock掉get_age方法,让它返回20
        p.get_age = mock.Mock(return_value=20)
        self.assertEqual(p.get_age(), 20)

    def test_should_get_fullname(self):
        p = Person()

        # mock掉get_fullname,让它返回'Tracy Cheng'
        p.get_fullname = mock.Mock(return_value='Tracy cheng')
        self.assertEqual(p.get_fullname(), 'Tracy cheng')

if __name__ == '__main__':
    unittest.main()

返回固定值时,按照我们上面的名词解释,算是Stub的一种用法,只是用Mock类来实现的。

2). 使用side_effect,依次返回指定值:

class PersonTest(unittest.TestCase):
    def test_should_get_age(self):
        p = Person()
        
        p.get_age = mock.Mock(side_effect=[10, 11, 12])

        self.assertEqual(p.get_age(), 10)
        self.assertEqual(p.get_age(), 11)
        self.assertEqual(p.get_age(), 12)

get_page()每一次被调用的时候都会到Mock的side_effect中去取一个值。如果调用次数超过了side_effect中的个数,程序运行时会报错StopIteration。

3). 打算输出为异常时:

p.get_age = mock.Mock(return_value =30,side_effect=Exception('Boom!'))

self.assertRaises(TypeError,p.get_age)

只要调用就会抛出异常。

  1. 检验是否调用
    def test_should_validate_method_calling(self):
            p = Person()

            p.get_fullname = mock.Mock(return_value='Tracy cheng')

            # 没调用过
            p.get_fullname.assert_not_called()  # Python 3.5

            p.get_fullname('1', '2')

            # # 调用过任意次数
            # p.get_fullname.assert_called()  # Python 3.6
            # # 只调用过一次, 不管参数
            # p.get_fullname.assert_called_once()  # Python 3.6
            # 只调用过一次,并且符合指定的参数
            p.get_fullname.assert_called_once_with('1', '2')

            p.get_fullname('3', '4')
            # 只要调用过即可,必须指定参数
            p.get_fullname.assert_any_call('1', '2')

            # 重置mock,重置之后相当于没有调用过
            p.get_fullname.reset_mock()
            p.get_fullname.assert_not_called()

            # Mock对象里除了return_value, side_effect属性外,
            # called表示是否调用过,call_count可以返回调用的次数
            self.assertEqual(p.get_fullname.called, False)
            self.assertEqual(p.get_fullname.call_count, 0)

            p.get_fullname('1', '2')
            p.get_fullname('3', '4')
            self.assertEqual(p.get_fullname.called, True)
            self.assertEqual(p.get_fullname.call_count, 2)

其中的assert_called和assert_called_once是python3.6中的用法,注意一下Python的版本。


稍微高阶一丢丢的用法:
静态方法和模块方法需要用到Patch来mock。其中会用到Patch装修器,包含有: patch(), patch.object() and patch.dict().

patch和patch.object这两个函数都会返回一个mock内部的类实例,这个类是class _patch。返回的这个类实例既可以作为函数的装饰器,也可以作为类的装饰器,也可以作为上下文管理器。使用patch或者patch.object的目的是为了控制mock的范围,意思就是在一个函数范围内,或者一个类的范围内,或者with语句的范围内mock掉一个对象。

# 在patch中给出定义好的Mock的对象,好处是定义好的对象可以复用

    def test_should_get_class_name(self):
        mock_get_class_name = mock.Mock(return_value='Man')
        with mock.patch.object(Person,'get_class_name',mock_get_class_name):
            self.assertEqual('Man',Person.get_class_name())

当你知道了mock能做什么之后,要如何学习并掌握mock呢?最好的方式就是查看阅读官方文档,并在自己的单元测试中使用。

也有一些大神已经封装出更好使用的第三方Python Mock库,可参见:
Python中好用的第三方mock库-httmock

拓展:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容