自动化测试中,我们常会使用一些经过简化的,行为与表现类似于生产环境下的对象的复制品。引入这样的复制品能够降低构建测试用例的复杂度,允许我们独立而解耦地测试某个模块,不再担心受到系统中其他部分的影响。
在《The Art of Unit Testing》书中Mock 被描述为假对象,通过验证是否发生与对象的交互来帮助确定测试是否失败或通过。其他的东西都被定义为Stub。在这本书中,Fake对象就是不真实的,根据它们的使用情况,它们可以是Stub,也可以是Mock。
更复杂一点的定义是Gerard Meszaros 在XunitPatterns中对此类对象的定义。他对这类对象统一称呼为:Test Double。包含:Dummy,Fake,Spy,Mock和Stub。
而通常,测试人员更倾向于使用 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对象的一般用法是这样的:
- 找到你要替换的对象,这个对象可以是一个类,或者是一个函数,或者是一个类实例。
- 实例化Mock类得到一个mock对象,并且设置这个mock对象的行为,比如被调用的时候返回什么值,被访问成员的时候返回什么值等。
- 使用这个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)
只要调用就会抛出异常。
- 检验是否调用
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
拓展:
Python3 mock模块 -Python官网
Test Double - Martin Fowler
Test Double - xUnit Patterns
Mocks Aren't Stubs - Martin Fowler
Command Query Separation - Martin Fowler