楔子
现在, 要测试这些接口:
""" 用例集 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/sundawei7/p/11949153.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: ests>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,这一切就都好办了,想要什么返回就模拟什么返回,妈妈再也不用担心你的测试覆盖度了。
关于TDD,see also:https://baike.baidu.com/item/TDD/9064369?fr=aladdin关于测试覆盖,see also:https://www.cnblogs.com/sundawei7/p/11944489.html
如何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: ests>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_with assert_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_with assert_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_call assert_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_calls assert_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_mock acttach_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_spec mock_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_mock reset_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
返回了两个方法。
[Mock测试概念介绍](https://www.jianshu.com/p/3944c0b82f30) | [如何 mock 数据](https://www.jianshu.com/p/63056120fab8) | [Python中的模块学习之mock模块](https://blog.csdn.net/peiyao456/article/details/77075173)