假设现在系统有两个模型A和B,其中A依赖B(例如A,B都是函数,A函数体内调用了B函数),但是B还没完成,或者根本就不在控制之内;这时候又需要对A的功能进行单独测试,就需要使用mock对象,模拟出一个假的fake_B模块,虽然这个fake_B模块是假的,但是我们可以通过对它的行为进行定制来使他能够看起来“像”B模块的功能,使A依赖fake_B,来对A的功能进行测试。同时,由于fake_B是完全可控的,除了可以定制B的属性,返回值之外,还可以对B模块的使用情况进行测试。
而在Python中可以使用mock和unittest.mock来产生这样的mock对象。
首先需要创建一个mock对象
>>>from mock import MagicMock
>>>fake_obj = MagicMock()
然后可以通过return_value设定它的返回值,当你调用这个mock对象时返回,返回值可以是任何类型,变量,函数,类对象都可以。
>>>fake_obj.return_value = 'This is a mock object'
>>>fake_obj()
'This is a mock object'
也可以通过side_effect指定它的副作用,这个副作用就是当你调用这个mock对象是会调用的函数,也可以选择抛出一个异常,来对程序的错误状态进行测试。
>>>def b():
... print 'This is b'
...
>>>fake_obj.side_effect = b
>>>fake_obj()
This is b
>>>fake_obj.side_effect = KeyError('This is b')
>>>fake_obj()
...
KeyError: 'This is b'
同时也可以以一个对象base为基础,拓展mock,使得mock获得base的所有属性和方法(但仅是接口相同,没有实现),当调用不属于base的属性和方法时,会抛出AttributeError。这需要在创建mock对象的时候,在spec参数指定,前面的返回值和副作用也可以在创建时进行指定。
>>>class B:
... def __init__(self):
... self.a = 'a'
...
>>>b = B()
>>>fake_obj = MagicMock(b)
'B' id='4370614160'>
>>>fake_obj.a
'a'
>>>fake_obj.b
AttributeError: Mock object has no attribute 'b'
还有一点需要强调的就是,如果要模拟一个对象而不是函数,你可以直接在mock对象上添加属性和方法,并且每一个添加的属性都是一个mock对象,也就是说可以对这些属性进行配置,并且可以一直递归的定义下去。
>>>fake_obj.fake_a.return_value = 'This is fake_obj.fake_a'
>>>fake_obj.fake_a()
'This is fake_obj.fake_a'
在对mock对象进行一定的配置之后就是使用mock对象进行测试,这个模块采用的‘action-assertion’的方式进行测试,即先运行,再通过断言进行测试。例如可以测试mock对象是否被调用,调用了几次,如何被调用等等,mock对象本身提供了丰富的断言方法,官方文档中提供了详尽的描述。
>>>fake_obj = MagicMock(return_value = 1)
>>>fake_obj.assert_called()
AssertionError: Expected 'None' to have been called.
>>>fake_obj()
>>>fake_obj.assert_called()
>>>
>>>fake_obj(1,2,3,key=1)
>>>fake_obj.assert_called_with(1,2,3,key=1)
>>>
同时mock库提供了patch函数来简化mock对象对原对象的替换。patch可以作为装饰器或者上下文管理器使用,这意味着在装饰的函数和上下文管理器中,对应的类会被替换为mock对象。
>>>from mock import patch
>>>@patch('__main__.SomeClass')
... def function(normal_argument, mock_class):
... print(mock_class is SomeClass)
...
>>>function(None)
True
官方文档 https://docs.python.org/3/library/unittest.mock.html