文章目录
- 1. mock 简述
- 2. mock 模块简介
- 2.1 构造器
- name 参数
- spec 参数
- return_value
- side_effect
- 2.2 断言方法
- 3. mock 示例
- 3.1 使用 Mock 类,返回固定值
- 3.2 校验参数个数,再返回固定值
- 3.3 使用 side_effect,依次返回指定值
- 3.4 根据不同的参数,返回不同的值
- 3.5 抛出异常
1. mock 简述
mock
模块是 Python 的测试框架 unittest
下的一个子包,是单元测试的一个重要模块。
单词 mock 有模拟的意思。在 Python 中,mock
可以理解为模拟一个方法、一个对象或者一个类等等,然后使用模拟的对象替换系统的一部分,对系统的一个单元进行测试,并对它们已使用的方式进行断言。
那为什么要使用 mock
呢?
因为,在实际生产中的项目非常复杂,对其进行单元测试的时候,会遇到以下问题:
- 接口的依赖
- 外部接口调用
- 测试环境非常复杂
单元测试应该只针对当前单元进行测试,所有的内部或外部的依赖应该是稳定的。使用 mock
就可以对外部依赖组件实现进行模拟和替换,从而使得单元测试将焦点只放在当前的单元功能。
场景模拟:比如有 A 和 B 两个模块,A 模块中有调用到 B 模块的方法,但是现在,B 模块中被 A 模块调用的方法需要修改,而我们又不想让它影响 A 模块的功能测试,所以就用到了单元测试模块 unittest 中的 mock 模块,mock 模块就是模拟出一个假的 B 模块供 A 模块测试用。
2. mock 模块简介
mock
对象的属性如下:
from unittest.mock import MagicMock
obj = MagicMock()
print(dir(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']
mock
对象的属性可以分为四类:
- 构造器;
- 断言方法;
- 管理方法;
- 静态方法。
具体如下图所示。
2.1 构造器
mock
的构造器有四个参数:name
、spec
、return_value
和 side_effect
。
name 参数
name
是mock
对象的名称,是mock
对象的唯一标识。
示例:
from unittest.mock import Mock
mock_obj = Mock(name='ok')
print(mock_obj)
print(repr(mock_obj))
输出:
<Mock name='ok' id='140435544582752'>
<Mock name='ok' id='140435544582752'>
可以看到,name
标识了唯一的一个 mock
(打印的时候,后边会显示 ID)。其中,repr(object)
方法返回对象的字符串形式。
spec 参数
spec
设置的是mock
对象的属性,可以是 property 或者方法,也可以是其他的列表字符串或者其他的 python 类。
spec
指定的是属性组成的 list:
from unittest.mock import Mock
list = ['kobe', 'curry']
mock_foo = Mock(spec=list)
print(mock_foo)
print(mock_foo.kobe)
print(mock_foo.curry)
输出:
<Mock id='139824239839840'>
<Mock name='mock.kobe' id='139824173697392'>
<Mock name='mock.curry' id='139824173697776'>
如果我们打印一个不存在的属性,如下
print(mock_foo.currycc)
则会报一个属性不存在的错误,如下
AttributeError: Mock object has no attribute 'currycc'
spec
指定的是一个类属性:
如下,创建一个 People
类,有 3 个属性(一个变量和两个方法)。
from unittest.mock import Mock
class People:
_num = 24
def f1(self):
print('f1()')
def f2(self, value):
print('value = ', value)
mock_foo = Mock(spec=People)
print(mock_foo)
print(mock_foo._num)
print(mock_foo.f1())
print(mock_foo.f2())
将 spec 设置为一个 People
对象,这个 mock 对象就有三个属性了。输出如下:
<Mock spec='People' id='140560969070672'>
<Mock name='mock._num' id='140560969127056'>
<Mock name='mock.f1()' id='140560969129888'>
<Mock name='mock.f2()' id='140560969238224'>
return_value
return_value
设置的是mock
对象被调用时的返回值。当这个mock
对象被调用的时候,显示出的结果就是return_value
的值。
方式一:使用 return_value
指定一个值
from unittest.mock import Mock
mock_foo = Mock(return_value = 24)
print(mock_foo)
mock_obj = mock_foo()
print(mock_obj)
输出:
<Mock id='140403200965216'>
24
当我们调用 mock
对象的时候,显示的就是 return_value
的值(也就是说 mock_obj 是带有一定的功能的)。
方式二:使用 return_value
指定一个对象
from unittest.mock import Mock
class People:
_num = 24
def f1(self):
print('f1()')
def f2(self, value):
print('value = ', value)
p = People()
# 打印对象p的地址
print(p)
# 设置return_value为对象p
mock_p = Mock(return_value=p)
print(mock_p)
# 调用mock对象,会返回上面设置的对象p
mock_obj = mock_p()
print(mock_obj)
print('================')
# 调用mock的对象p的属性和方法
print(mock_obj._num)
print(mock_obj.f1())
# 调用方法也需要传入参数
print(mock_obj.f2(8))
<__main__.People object at 0x7f7bb4707e20>
<Mock id='140169218681200'>
<__main__.People object at 0x7f7bb4707e20>
================
24
f1()
None
value = 8
None
side_effect
side_effect
是和return_value
是相反的,它会覆盖return_value
的值。也就是说当这个mock
对象被调用的时候,返回的是side_effect
的值,而不是return_value
的值。
side_effect
指定一个 list 或者 tuple 作为参数:
from unittest.mock import Mock
list = [1, 2, 3]
mock_foo = Mock(return_value=100, side_effect=list)
mock_obj = mock_foo()
print(mock_obj)
mock_obj = mock_foo()
print(mock_obj)
mock_obj = mock_foo()
print(mock_obj)
mock_obj = mock_foo()
print(mock_obj)
输出:
Traceback (most recent call last):
File "/home/10307952@zte.intra/PyProjects/py_note/my_note/mock_test/mock_init/init_side_effect.py", line 12, in <module>
mock_obj = mock_foo()
File "/usr/local/python38/lib/python3.8/unittest/mock.py", line 1081, in __call__
return self._mock_call(*args, **kwargs)
File "/usr/local/python38/lib/python3.8/unittest/mock.py", line 1085, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/usr/local/python38/lib/python3.8/unittest/mock.py", line 1142, in _execute_mock_call
result = next(effect)
StopIteration
1
2
3
注:
side_effect
指定一个 list 或者 tuple 作为参数后,会返回一个迭代器,来替换 return_value
的值。每执行一次,会返回 list 中的一个值,返回完的时候就会抛出异常。(类似于 Python 的生成器,每次执行 next 操作,都会返回一个值,当返回完的时候,就会抛出异常)
2.2 断言方法
assert_called_with(*argSeq, **argKW)
:检查 mock 方法是否获得了正确的参数。
当至少有一个参数有错误的值或者类型时,当参数的个数出错时,当参数的顺序不正确时,这个断言就会发生错误。
如下,方法 f1()
不需要参数,使用 mock 对象调用 f1()
之后,使用 assert_called_with()
断言成功。
from unittest.mock import Mock
class People:
_num = 24
def f1(self):
print('f1()')
def f2(self, value):
print('value = ', value)
mock_foo = Mock(spec=People)
print(mock_foo)
mock_foo.f1()
mock_foo.f1.assert_called_with()
方法 f2()
需要一个参数,如果我们使用 mock 对象调用 f1()
时不传入参数,使用 assert_called_with()
则会断言失败。如下
mock_foo.f2()
mock_foo.f2.assert_called_with(10)
错误信息如下:
AssertionError: expected call not found.
Expected: f1()
Actual: f1(1)
assert_called_once_with(*argSeq, **argKW)
:当某个方法被多次调用的时候,它就会报错。
在上面的基础上,我们再调用一次方法 f2()
,然后使用 assert_called_once_with(10)
断言,如下
mock_foo.f2(10)
mock_foo.f2.assert_called_once_with(10)
方法 f2()
被调用了两次,断言信息如下:
AssertionError: Expected 'f2' to be called once. Called 2 times.
Calls: [call(), call(10)].
assert_any_call()
:用于检查测试的 mock 对象在测试例程中是否调用了方法。
示例:
# 先用mock对象调用两次f2(),第一次不传参,第二次传参数10
mock_foo.f2()
mock_foo.f2(10)
# 断言mock对象是否调用了不带参数的f2()方法,断言成功
mock_foo.f2.assert_any_call()
# 断言mock对象是否调用了参数为200的f2()方法,断言失败
mock_foo.f2.assert_any_call(200)
断言失败信息如下:
AssertionError: f2(200) call not found
assert_has_calls()
:检查是否按照正确的顺序和正确的参数调用方法。所以,我们需要给出一个方法的调用顺序,assert 的时候按照这个顺序进行检查。
示例:
# 调用方法
mock_foo.f1()
mock_foo.f2()
mock_foo.f2(10)
# 按照上面的调用顺序进行断言,断言成功
foo_calls = [call.f1(), call.f2(), call.f2(10)]
mock_foo.assert_has_calls(foo_calls)
更换 foo_calls 指定的调用顺序,则断言失败,如下:
foo_calls = [call.f1(), call.f2(10), call.f2()]
mock_foo.assert_has_calls(foo_calls)
# 断言失败信息如下
AssertionError: Calls not found.
Expected: [call.f1(), call.f2(10), call.f2()]
Actual: [call.f1(), call.f2(), call.f2(10)]
3. mock 示例
先定义一个类,有两个成员方法(一个有参数,一个无参数),还有一个静态方法。如下:
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__
3.1 使用 Mock 类,返回固定值
使用 return_value
指定返回值。
import unittest
from unittest import TestCase
from unittest.mock import Mock
from my_note.mock_test.mock_example.person import Person
class PersonTest(TestCase):
def test_get_age(self):
p = Person()
# 不使用mock,正常调用get_age()
self.assertEqual(p.get_age(), 10)
# mock掉get_age(),让它返回20,使用return_value指定返回值
p.get_age = Mock(return_value=20)
self.assertEqual(p.get_age(), 20)
def test_get_full_name(self):
p = Person()
# 正常调用get_fullname()
print(p.get_fullname('kobe', 'bryant'))
# mock掉get_fullname(),
p.get_fullname = Mock(return_value='stephen curry')
self.assertEqual(p.get_fullname(), 'stephen curry')
if __name__ == '__main__':
unittest.main()
需要注意的是,上面的示例在调用 p.get_fullname
时没有给任何的参数,但是依然可以工作。并且,任意指定参数对结果并没有影响。如下:
self.assertEqual(p.get_full_name('1'), 'stephen curry')
self.assertEqual(p.get_full_name('1', '2'), 'stephen curry')
self.assertEqual(p.get_full_name('1', '2', '3'), 'stephen curry')
也就是说这种方式无法校验参数,如果想校验参数需要用 create_autospec
模块方法替代 Mock
类。
3.2 校验参数个数,再返回固定值
def test_get_full_name2(self):
p = Person()
# 任意给定两个参数,依然会返回mock的值,但参数个数必须对
p.get_full_name = create_autospec(p.get_full_name, return_value='stephen curry')
self.assertEqual(p.get_full_name('1', '2'), 'stephen curry')
# 如果参数个数不对,会报错TypeError: missing a required argument: 'first_name'
self.assertEqual(p.get_full_name(), 'stephen curry')
3.3 使用 side_effect,依次返回指定值
可以指定一组返回值,然后使用 side_effect
依次返回。
def test_get_age2(self):
p = Person()
# 指定一组返回值,side_effect会覆盖return_value的值
list = [2, 4, 6]
p.get_age = Mock(return_value=20, side_effect=list)
self.assertEqual(p.get_age(), 2)
self.assertEqual(p.get_age(), 4)
self.assertEqual(p.get_age(), 6)
3.4 根据不同的参数,返回不同的值
def test_get_full_name3(self):
p = Person()
values = {('kobe', 'bryant'): 'kobe bryant', ('stephen', 'curry'): 'stephen curry'}
p.get_full_name = Mock(side_effect=lambda x, y: values[(x, y)])
self.assertEqual(p.get_full_name('stephen', 'curry'), 'stephen curry')
self.assertEqual(p.get_full_name('kobe', 'bryant'), 'kobe bryant')
3.5 抛出异常
def test_raise_exception(self):
p = Person()
p.get_age = Mock(side_effect=TypeError('integer type'))
# 只要调用get_age()方法,就会抛出异常
self.assertRaises(TypeError, p.get_age())
这里调用了 p.get_age()
,抛出异常如下:
TypeError: integer type