Mock
Mock
"""
用例集
case_set.py
pip install requests
"""
import requests
def v2ex_info():
"""
获取v2ex的网站信息
https://www.v2ex.com/api/site/info.json
"""
response = requests.get(url='https://www.v2ex.com/api/site/info.json')
return response.json().get('title') # V2EX
def v2ex_stats():
"""
获取v2ex的网站信息
https://www.v2ex.com/api/site/stats.json
"""
response = requests.get(url='https://www.v2ex.com/api/site/stats.json')
return response.json().get('member_max') # int类型
def cnodejs():
""" 获取 cnodejs,推荐博客总数 """
response = requests.get('https://cnodejs.org/api/v1/topics')
return response.json().get('success') # True
if __name__ == '__main__':
print(v2ex_info() == 'V2EX')
print(type(v2ex_stats()) is int)
print(cnodejs() is True)
关于requests模块, see also:https://www.cnblogs.com/Neeo/articles/11511087.html
规则是:
- v2ex_info接口返回值中的title是
V2EX
才算通过。 - v2ex_stats接口返回值中的member_max是int类型才算通过。
- cnodejs接口只值中的success是True算通过。
你根据规则很快的写出了测试用例:
"""
用例类
myMain.py
"""
import unittest
from case_set import v2ex_stats, v2ex_info, cnodejs
class InterfaceCase(unittest.TestCase):
def test_v2ex_stats(self):
""" 测试 v2ex_stats 接口,返回: int类型"""
self.assertIs(type(v2ex_stats()), int)
def test_v2ex_info(self):
""" 测试 v2ex_info 接口, 返回: V2EX """
self.assertEqual(v2ex_info(), 'V2EX')
def test_cnodejs(self):
""" 测试 cnodejs 接口,返回: True """
self.assertIs(cnodejs(), True)
if __name__ == '__main__':
unittest.main()
结果也OK:
M:\tests>python36 myMain.py -v
test_cnblogs_info (__main__.InterfaceCase)
测试博客园接口,返回: 200 ... ok
test_v2ex_info (__main__.InterfaceCase)
测试 v2ex_info 接口, 返回: V2EX ... ok
test_v2ex_stats (__main__.InterfaceCase)
测试 v2ex_stats 接口,返回: int类型 ... ok
----------------------------------------------------------------------
Ran 3 tests in 3.075s
OK
完美,你就准备去泡妞了......
为什么需要mock
桥逗麻袋!你在反复的执行测试用例时,发现test_cnodejs
用例执行有些问题。有时候执行失败有时候成功,并且就算成功也响应时间较长,一番分析后,发现不是自己的问题,是接口暂时开发的不是很完善,导致现在测试不稳定。
但是你根据接口文档知道,这个接口这么测试,只要返回True就算通过。
那么能不能我们自己模拟出来这么一个接口,然后模拟一些方法和数据,在测试环境下使用。
什么是mock
在协同开发、测试中,总会出现各种问题,比如:
- 开发人员某些接口还没有开发完毕。
- 与第三方联调时,第三方拖了后腿,没准备好环境、数据都有可能。比如说我们测试的某个接口本身没有问题,但它依赖的某个接口有些问题,这就影响我们的正常测试任务进度。
- 测试环境恶劣。
- 开发只提供接口,数据自己搞!
这些问题总能影响我们的测试进度,那么我们怎么正常的展开呢?
这就需要mock来解决了。
什么是mock
mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。
mock测试一般也称为mock数据。
简单来说,mock就是向测试对象提供一套和测试资源完全相同的接口和方法,不关系具体的实现过程,只关心具体结果。
mock测试的优点
- 团队并行工作:有了mock,前后端人员只需要定义好接口文档就可以开始并行的工作,互不影响,只需要在最后联调的时候多多交流即可。后端与后端之间如果有接口耦合,也同样能被Mock解决;测试过程中如果遇到依赖接口没有准备好,同样可以借助Mock;不会出现一个团队等待另一个团队的情况。这样的话,开发自测阶段就可以及早开展,从而发现缺陷的时机也提前了,有利于整个产品质量以及进度的保证。
- 开启TDD模式,即测试驱动开发:单元测试是TDD实现的基石,而TDD经常会碰到协同模块尚未开发完成的情况,但是有了mock,这些一切都不是问题。当接口定义好后,测试人员就可以创建一个Mock,把接口添加到自动化测试环境,提前创建测试。
- 模拟出无法访问的资源:比如说,你需要调用一个“墙”外的资源来方便自己调试,就可以自己Mock一个。
- 系统隔离:假如我们需要调用一个post请求,为了获得某个响应,来看当前系统是否能正确处理返回的“响应”,但是这个post请求会造成数据库中数据的污染,那么就可以充分利用Mock,构造一个虚拟的post请求,我们给他指定返回就好了。
- 产品展示:假如我们需要创建一个演示程序,并且做了简单的UI,那么在完全没有开发后端服务的情况下,也可以进行演示。说到演示了,假如你已经做好了一个系统,并且需要给客户进行演示,但是里面有些真实数据并不想让用户看到,那么同样,你可以用Mock接口把这些敏感信息接口全部替换。
- 测试覆盖:假如有一个接口,有100个不同类型的返回,我们需要测试它在不同返回下,系统是否能够正常响应,但是有些返回在正常情况下基本不会发生,难道你要千方百计地给系统做各种手脚让他返回以便测试吗?比如,我们需要测试在当接口发生500错误的时候,app是否崩溃,别告诉我你一定要给服务端代码做些手脚让他返回500 。。。而使用mock,这一切就都好办了,想要什么返回就模拟什么返回,妈妈再也不用担心你的测试覆盖度了。
如何mock数据
下载安装
这里需要用到mock模块了,在Python3.x中,mock被集成到了unittest中,无需下载,直接导入即可,但在Python2.x中,就需要:
pip install mock
mock类的构成
这里以Python3.x为例。
快速上手
构造器:_init_
from unittest import mock
mock_obj = mock.Mock()
print(mock_obj) # <Mock id='10069264'>
print(dir(mock_obj))
'''
[
'assert_any_call', 'assert_called', 'assert_called_once',
'assert_called_once_with', 'assert_called_with', 'assert_has_calls',
'assert_not_called', 'attach_mock', 'call_args',
'call_args_list', 'call_count', 'called',
'configure_mock', 'method_calls', 'mock_add_spec',
'mock_calls', 'reset_mock', 'return_value',
'side_effect'
]
'''
虽然__init__
是实例化方法,但在这里通常被称为构造器。
由打印结果可以看到,通过mock.Mock()
实例化出一个mock对象mock_obj
。这个对象是继承了Mock类的属性和方法。这样的一个mock对象对我们来说用处不大。
我们来试着添加一些自定义属性和方法,使之更灵活。
在Mock实例化时,我们可以传入这些参数:
- name:mock对象的名字。它只是起到标识作用,当你print一个有name的mock对象时,可以看到它的name。
- spec:mock对象的属性值。
- side_effect:该参数指向一个可调用对象(一般是函数),当mock对象被调用时,如果该参数的返回值是默认的DEFAULT,则mock对象返回return_value指定的值,否则返回side_effect指定的对象的返回值。
- return_value:该参数指定一个值或者对象,当mock对象被调用时,如果side_effect函数的返回值是DEFAULT,那么mock对象返回return_value指定的值或者对象。
注意,如果side_effect和return_value同时存在的时候,side_effect将会覆盖return_value。
name
from unittest import mock
mock_obj1 = mock.Mock()
mock_obj2 = mock.Mock(name='mock_obj2')
print(mock_obj1) # <Mock id='50111760'>
print(mock_obj2) # <Mock name='mock_obj' id='53781776'>
name
参数没啥好说的,就是跟mock对象起了个名字。
为return_value指定某个值
现在让我们使用mock来模拟出文章开头的那几个接口测试中的cnodejs
接口。
import unittest
from unittest import mock
from case_set import cnodejs # 导入真实的cnodejs接口函数
class CnodejsTestCase(unittest.TestCase):
def test_mock_cnodejs(self):
""" 使用 mock 模拟的 cnodejs 接口 返回: True"""
# 构造mock对象
cnodejs = mock.Mock(return_value=True)
# 使用mock对象进行断言
self.assertIs(cnodejs(), True)
def test_cnodejs(self):
""" 测试 cnodejs 接口,返回: True """
self.assertIs(cnodejs(), True)
if __name__ == '__main__':
unittest.main()
用例test_mock_cnodejs
方法中:
在Mock类实例化时传入return_value
参数,然后构造出的mock对象赋值给cnodejs
变量。然后cnodejs()
相当于调用mock对象,得到返回值True
,完事拿着这个返回值使用unittest进行断言。
用例test_cnodejs
方法中,正常写测试用例断言,以判断两个用例方法有什么不同之处:
test_cnodejs (__main__.CnodejsTestCase)
测试 cnodejs 接口,返回: True ... ok
test_mock_cnodejs (__main__.CnodejsTestCase)
使用 mock 模拟的 cnodejs 接口 返回: True ... ok
----------------------------------------------------------------------
Ran 2 tests in 1.097s
OK
可以看到,两个用例方法都通过了,并没有什么区别。
在测试环境下,我们使用mock模拟的方法进行测试,这样能尽早的介入测试,带来的优势不一而足。
为return_value指定类的对象return_value
除了上述用法,还可以指定类的对象:
from unittest import mock
class Foo(object):
""" 自定义类 """
def f1(self):
return 'this is Foo.f1'
def f2(self, name):
return name
# 正常的类的实例化与调用
foo_obj = Foo()
print(foo_obj.f1()) # this is Foo.f1
print(foo_obj.f2('this is Foo.f2')) # this is Foo.f2
# 构造mock对象并传入 Foo实例化对象
foo_Class = mock.Mock(return_value=Foo())
# 想要得到mock对象的返回值,必须调用,也就是加括号
foo_obj = foo_Class() # mock对象调用得到return_value值也就是Foo的实例化对象
# 接下里就是正常的调用了
print(foo_obj.f1()) # this is Foo.f1
# 同样可以正常传参
print(foo_obj.f2('this is Foo.f2')) # this is Foo.f2
使用mock对象模拟类的实例化对象同样方便。
side_effect
先来看第一个示例,我们可以为mock对象的side_effect
参数指定可迭代对象。
from unittest import mock
mock_obj1 = mock.Mock(return_value=100)
print(mock_obj1()) # 100
mock_obj2 = mock.Mock(return_value=100, side_effect=[200, 300])
print(mock_obj2()) # 200
由上例可以看到,如果在构造mock对象的时候,只有return_value
被指定,调用mock对象返回return_value
指定的值。
当side_effect
和return_value
同时被指定时,side_effect
就覆盖了return_value
。
那么既然side_effect
接受的是一个可迭代对象,我们就可以多次调用它:
from unittest import mock
mock_obj1 = mock.Mock(return_value=100)
print(mock_obj1()) # 100
mock_obj2 = mock.Mock(return_value=100, side_effect=[200, 300])
print(mock_obj2()) # 200
print(mock_obj2()) # 300
print(mock_obj2()) # StopIteration
可以看到side_effect
对象本质上是一个生成器。
为spec指定属性组成的列表
现在我们使用mock来模拟出来两V2EX的两个接口方法。
import unittest
from unittest import mock
from case_set import v2ex_info, v2ex_stats
# 为mock对象的spec参数传入属性(方法)组成的列表
spec_list = ['v2ex_info', 'v2ex_stats']
mock_obj = mock.Mock(spec=spec_list)
print(spec_list) # ['v2ex_info', 'v2ex_stats']
# 根据真实的接口规则设置两个方法的返回值
mock_obj.v2ex_info.return_value = 'V2EX'
mock_obj.v2ex_stats.return_value = 466668 # 该接口只需要返回值是int即可
class TestCaseDemo(unittest.TestCase):
def test_v2ex_stats(self):
""" 测试 v2ex_stats 接口,返回: int类型"""
self.assertIs(type(v2ex_stats()), int)
def test_mock_v2ex_stats(self):
""" mock v2ex_stats 接口,返回: int类型 """
v2ex_stats = mock_obj.v2ex_stats
self.assertIs(type(v2ex_stats()), int)
def test_mock_v2ex_info(self):
""" mock v2ex_info 接口, 返回: V2EX """
v2ex_info = mock_obj.v2ex_info
self.assertEqual(v2ex_info(), 'V2EX')
def test_v2ex_info(self):
""" 测试 v2ex_info 接口, 返回: V2EX """
self.assertEqual(v2ex_info(), 'V2EX')
if __name__ == '__main__':
unittest.main()
结果:
M:\tests>python36 myMain.py -v
test_mock_v2ex_info (__main__.TestCaseDemo)
mock v2ex_info 接口, 返回: V2EX ... ok
test_mock_v2ex_stats (__main__.TestCaseDemo)
mock v2ex_stats 接口,返回: int类型 ... ok
test_v2ex_info (__main__.TestCaseDemo)
测试 v2ex_info 接口, 返回: V2EX ... ok
test_v2ex_stats (__main__.TestCaseDemo)
测试 v2ex_stats 接口,返回: int类型 ... ok
----------------------------------------------------------------------
Ran 4 tests in 3.154s
OK
由结果发现,我们用mock模拟的两个接口都通过了。
为spec指定类属性
from unittest.mock import Mock
class Foo(object):
age = 20
def f1(self):
return 'this if f1'
def f2(self, name):
return name
mock_obj = Mock(spec=Foo)
print(mock_obj.f1) # <Mock name='mock.f1' id='1847131683640'>
print(mock_obj.f2) # <Mock name='mock.f2' id='1847131615128'>
print(mock_obj.age) # <Mock name='mock.age' id='1847131718880'>
print(mock_obj.name) # AttributeError: Mock object has no attribute 'name'
我们为mock对象指定了属性为Foo类,那么,类中的方法和属性都是mock对象的属性,这也是前三个打印没有问题的原因,而第4个打印报错了,显然,Foo类中没有一个叫name的属性或者方法。
mock断言语句
由mock思维导图我们知道,mock关于断言有这些常用的:
- assert_called_with(arg):检查函数调用参数是否正确。
- assert_called_once_with(arg):检查函数调用参数是否正确,但是只调用一次。
- assert_any_call():用于检查测试的mock对象在测试例程中是否调用了方法。
- assert_has_calls():期望调用方法列表。
assert_called_withassert_called_with
检查mock方法是否获取了正确的参数,当至少有一个参数有错误的值或者类型时、当参数的个数出错时、当参数的顺序不正确时,断言失败。
from unittest.mock import Mock
class Foo(object):
value = 20
def f1(self, arg):
return arg
def f2(self, *args):
return args
mock_obj = Mock(spec=Foo)
# f1正确的传参姿势
mock_obj.f1(222)
# mock_obj.f1.assert_called_with() # 报错,没有传参
# mock_obj.f1.assert_called_with(11) # 报错,瞎98传参
# mock_obj.f1.assert_called_with('6669') # 报错,6翻了吧,传值的类型不对
# mock_obj.f1.assert_called_with(222) # 噢啦,mock_obj.f1()传的就是 222
# f2正确传参姿势
mock_obj.f2(1, 2, 3)
# mock_obj.f2.assert_called_with() # 报错,没有传参
# mock_obj.f2.assert_called_with(1) # 报错,少传了参数
# mock_obj.f2.assert_called_with(1, 3, 2) # 报错,传参顺序不对
mock_obj.f2.assert_called_with(1, 2, 3) # 噢啦,传参姿势很对
啥都不用说了吧,都在代码里。
assert_called_once_withassert_called_once_with
断言,当指定方法被多次调用的时候,断言失败。
from unittest.mock import Mock
class Foo(object):
value = 20
def f1(self, arg):
return arg
def f2(self, *args):
return args
# 实例化mock对象
mock_obj = Mock(spec=Foo)
# 为f1方法赋返回值
mock_obj.f1.return_value = 222
print(mock_obj.f1())
mock_obj.f1.assert_called_once_with() # 第一次调用,没问题
print(mock_obj.f1())
mock_obj.f1.assert_called_once_with() # 第二次调用,报错 AssertionError: Expected 'f1' to be called once. Called 2 times.
这个断言相对简单。
assert_any_callassert_any_call
断言用于检查测试执行中的mock对象在测试中是否调用了方法。
from unittest.mock import Mock
class Foo(object):
value = 20
def f1(self, arg):
return arg
def f2(self, *args):
return args
mock_obj = Mock(spec=Foo)
# mock对象调用了 f1() f1(100) f1(200) f1(200)
mock_obj.f1()
mock_obj.f1(100)
mock_obj.f1(200)
mock_obj.f1(200)
# 判断:mock对象调用了f1() f1(100) f1(200) f1(300) f2()
mock_obj.f1.assert_any_call() # 没错
mock_obj.f1.assert_any_call(100) # 没错
mock_obj.f1.assert_any_call(200) # 没错
# mock_obj.f1.assert_any_call(300) # AssertionError: f1(300) call not found
mock_obj.f2.assert_any_call() # AssertionError: f2() call not found
上例,assert_any_call
会判断整个测试中方法是否被调用了。而不管该方法是否被重复调用。
例如,在程序执行时执行了mock_obj.f1.assert_any_call()
,那么就用mock_obj.f1.assert_any_call()
判断刚才的方法是否执行过。执行过啥都不做,要是没执行过就报错。
assert_has_callsassert_has_calls
检查是否按照正确的顺序和正确的参数进行调用的。所以,我们需要给出一个方法的调用顺序,assert的时候按照这个顺序进行检查。
from unittest.mock import Mock
from unittest.mock import call # 引入新的模块
class Foo(object):
value = 20
def f1(self, arg):
return arg
mock_obj = Mock(spec=Foo)
# 正确的执行顺序是 f1() f1(100) f1(200)
mock_obj.f1()
mock_obj.f1(100)
mock_obj.f1(200)
# 报错, 现在的执行顺序是 f1() f1(100) f1(300)
# calls_list = [call.f1(), call.f1(100), call.f1(300)] # 报错,没有 call.f1(300)
# mock_obj.assert_has_calls(calls_list)
# 报错,现在的执行顺序是 f1(200) f1() f1(300)
# calls_list = [call.f1(200), call.f1(), call.f1(200)] # 报错,执行顺序不对
# mock_obj.assert_has_calls(calls_list)
# 对喽
calls_list = [call.f1(), call.f1(100), call.f1(200)]
mock_obj.assert_has_calls(calls_list)
首先,我们以列表的形式列出方法调用顺序,每个方法前使用call.f1()
的形式,因为如果不加call
来修饰的话, 解释器将不知道f1
是一个方法,当然call
在使用之前需要引入。
mock管理方法
mock中,关于管理有这些常用方法:
- attach_mock:将一个mock对象添加到另一个mock对象中。
- configure_mock,更改mock对象的return_value值。
- mock_add_spec:给mock对象添加新的属性。
- reset_mock:将mock对象恢复到初始状态。
acttach_mockacttach_mock
将一个mock对象添加到另一个mock对象中。
from unittest.mock import Mock
class Foo(object):
def f1(self, arg):
return arg
class Bar(object):
def f2(self, *args):
pass
# 分别构造foo和bar的mock对象
mock_foo = Mock(spec=Foo)
mock_bar = Mock(spec=Bar)
# 打印也没问题
print(mock_foo, mock_bar) # <Mock spec='Foo' id='57738096'> <Mock spec='Bar' id='130627728'>
# 分别为两个mock对象的方法添加返回值
mock_foo.f1.return_value = 'Foo.f1'
mock_bar.f2.return_value = 'Bar.f2'
# 正常的调用都没问题
print(mock_foo.f1()) # Foo.f1
print(mock_bar.f2()) # Bar.f2
# 使用attach_mock将mock_bar对象添加到mock_foo中
mock_foo.attach_mock(mock_bar, 'bar')
# 现在mock_bar对象成为了mock_foo mock对象的一个属性bar
print(mock_foo.bar) # <Mock name='mock.bar' spec='Bar' id='132987120'>
# mock_foo.bar等于拿到了mock_bar对象,然后调用其中的f2方法,并且得到了之前赋值的返回值
print(mock_foo.bar.f2()) # Bar.f2
需要注意的是,attach_mock(self, mock, attribute)
必须为添加进来的mock对象指定一个属性名。
configure_mock
configure_mock
用来更改mock对象的return_value值。
from unittest.mock import Mock
class Foo(object):
def f1(self, arg):
return arg
def f2(self, arg):
return arg
# 实例化mock对象并添加属性和返回值
mock_obj = Mock(spec=Foo, return_value='abc')
# 正常调用mock对象得到预期的结果 abc
print(mock_obj()) # abc
# 使用configure_mock修改mock对象的return_value值
mock_obj.configure_mock(return_value='xyz')
# 修改成功
print(mock_obj()) # xyz
# 可以批量设置返回值,比如f1方法的返回值为 '100', f2方法的返回值为 200
spec_dict = {'f1.return_value': '100', 'f2.return_value': 200}
# 将字典打散后使用configure_mock设置到mock对象中
mock_obj.configure_mock(**spec_dict)
print(mock_obj()) # xyz
print(mock_obj.f1()) # 100 ps:字符串类型的100
print(mock_obj.f2()) # 2090
mock_add_specmock_add_spec(self, spec, spec_set=False)
用来给mock对象添加一个新的属性,新的属性会覆盖掉原来的属性。spec_set
指属性可读可写,默认是只读,但可写我没测试出来....欢迎留言指正。
from unittest.mock import Mock
class Foo(object):
def f1(self, arg):
return arg
class Bar(object):
def f2(self, args):
return args
def ace():
pass
# 实例化mock对象
mock_obj = Mock(spec=Foo)
print(mock_obj.f1()) # <Mock name='mock.f1()' id='119946576'>
# 使用mock_add_spec给mock_obj添加一个新的属性
mock_obj.mock_add_spec(Bar)
print(mock_obj.f2()) # <Mock name='mock.f2()' id='46952912'>
# 正常的使用都没问题
mock_obj.f2.return_value = 'Bar.f2'
print(mock_obj.f2()) # Bar.f2
# 上面添加的属性是类,现在是函数,记得函数这里没有方法,别瞎点啊
mock_obj.mock_add_spec(ace)
mock_obj.return_value = 'function'
print(mock_obj()) # function
# 另外,新添加的属性会覆盖掉之前的属性。现在的mock对象模拟的函数ace对象,ace函数哪有什么f1啊
print(mock_obj.f1()) # AttributeError: Mock object has no attribute 'f1'
reset_mockreset_mock
将mock对象回复到初识状态,避免了重新构造mock对象带来的开销。
from unittest.mock import Mock
class Foo(object):
def f1(self, arg):
return arg
mock_obj = Mock(spec=Foo)
mock_obj.f1()
# 这里如果不使用 reset_mock, 那么f1方法就被调用了两次,下面的 assert_called_once_with就会报错,现在则不报错了
mock_obj.reset_mock()
mock_obj.f1()
mock_obj.f1.assert_called_once_with()
mock统计方法
再来看mock关于统计的一些方法:
- called:跟踪mock对象所做的任意调用的访问器。
- mock_calls:显示工厂调用和方法调用。
- call_args:mock对象的初始化参数。
- call_args_list:调用中使用参数。
- call_count:mock对象被调用次数。
- method_calls:以列表的形式返回mock对象都调用了哪些方法。
called
from unittest.mock import Mock
def ace():
pass
# 构造 mock对象并没有调用
mock_obj = Mock(spec=ace)
# OK,此时mock对象没有调用,所以mock_obj.called:False
print(mock_obj.called) # False
# OK,现在调用了,那么 mock_obj.called:True
mock_obj()
print(mock_obj.called) # True
called
只要检测到mock对象被调用,就返回True。
call_count
from unittest.mock import Mock
def ace():
pass
mock_obj = Mock(spec=ace)
mock_obj()
mock_obj()
mock_obj()
print(mock_obj.call_count) # 3
call_count
检查mock对象被调用了多少次。
call_args && call_args_list
from unittest.mock import Mock
mock_obj = Mock()
mock_obj()
print(mock_obj.call_args) # call()
print(mock_obj.call_args_list) # [call()]
call_args_list
以列表的形式返回工厂调用时所有的参数。
method_calls
from unittest.mock import Mock
class Foo(object):
def f1(self, arg):
return arg
mock_obj = Mock(spec=Foo)
mock_obj()
mock_obj.f1()
print(mock_obj.call_args) # call()
print(mock_obj.call_args_list) # [call()]
print(mock_obj.method_calls) # [call.f1()]
mock_calls
from unittest.mock import Mock
class Foo(object):
def f1(self, arg):
return arg
mock_obj = Mock(spec=Foo)
mock_obj()
mock_obj.f1()
print(mock_obj.mock_calls) # [call(), call.f1()]
首先,mock对象被调用时,执行工厂call
方法,完事第二次调用了f1方法,所以mock_calls
返回了两个方法。