(2022.12.18 Sun)
Mock简单来理解,就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试。而这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
Python 3.3版本之前,mock是个独立模块,之后被整合进入python的unittest
模块中。
Mock基本用法
(2023.01.08 Sun)
unittest.mock
提供了用于模拟对象的基类Mock
。Mock
使用相当灵活,其用例相当广泛。
Mock
使用时首先实例化
>>> from unittest.mock import Mock
>>> mock = Mock()
>>> mock
之后就可以在代码中用Mock
实例代替对象了。代替方法:
- 将这个实例化的
mock
当做参数传递到函数中
# pass mock as an argument to a function do_sth()
do_sth(mock)
- 用实例化的
mock
重新定义对象
tmp = mock
Mock
看起来如同一个真实的对象,不然没有替代效果。比如上面的第二种方式mock
模拟tmp
对象,tmp
对象中有某方法dir
,则mock
对象也包含dir
方法。
惰性属性和方法lazy attributes and methods
Mock
对象对被替代的对象要完全替代。为了提高灵活性,对象的属性只有在调用/使用时才被创建。
>>> tmp.some_attribute
>>> tmp.some_method()
可见Mock
实例化对象可随时生成任何属性,适合代替各种对象。
对比被替代的对象的对应方法,Mock
版的对象有两个不同
- 模拟的对象
tmp
不需要输入变量(arguments),且它会接受传递进来的任何数目的变量 -
mock
对象的对应方法返回结果依然是Mock
对象,这种能力使用户可以定义复杂情形
>>> t1 = Mock()
>>> t1.load({'k': 123}).get('w')
Assertions and Inpspection
mock
对象的内置方法和属性如下
>>> t1 = Mock()
>>> dir(t1)
['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', 'load', 'method_calls', 'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value', 'side_effect']
上面内容中看到有若干方法以assert_
起始。用于统计调用信息。
>>> tmp = Mock() # create a mock object
>>> tmp.see('world')
>>> tmp.see.assert_called()
>>> tmp.see.assert_called()
>>> tmp.see.assert_called_once()
>>> tmp.see.assert_called_with('word')
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 907, in assert_called_with
raise AssertionError(_error_message()) from cause
AssertionError: expected call not found.
Expected: see('word')
Actual: see('world')
>>> tmp.see.assert_called_once_with('world')
>>> tmp.see('hello')
>>> tmp.see.assert_called_once()
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 886, in assert_called_once
raise AssertionError(msg)
AssertionError: Expected 'see' to have been called once. Called 2 times.
Calls: [call('world'), call('hello')].
>>> tmp.see.assert_not_called()
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 868, in assert_not_called
raise AssertionError(msg)
AssertionError: Expected 'see' to not have been called. Called 2 times.
Calls: [call('world'), call('hello')].
>>> tmp1 = Mock()
>>> tmp1.assert_called()
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 876, in assert_called
raise AssertionError(msg)
AssertionError: Expected 'mock' to have been called.
-
.assert_called()
:返回该Mock
实例化对象是否被调用过,如果是,则返回None,如果未被调用,则返回AssertionError
-
.assert_called_once()
:检测Mock
对象是否被调用一次 -
.assert_called_with(*args, **kwargs)
:检测是否以输入参数的形式调用过 -
.assert_called_once_with(*args, **kwargs)
:检测是否已输入参数的形式调用过一次
return_value
和side_effect
(2023.01.12 Thur)
Mock
类在实例化时可以针对其中的return_value
和side_effect
两个参数。
-
return_value
:Mock
实例的返回值,指定一个对象,当Mock
实例被调用,则返回其指定的值 -
side_effect
:Mock
实例的异常或动态改变值,可以是一个值,或一个可迭代序列对象。当Mock
实例时指定了side_effect
,则return_value
的参数就会失效,或者说两者在实例化时如果被同时指定,side_effect
的优先级更高
其中关于side_effect
,在运行函数时有时不只需要看其返回值。考虑到代码可能会改变环境,比如改变类的属性值,系统中的某个文件,或数据库中的字段。这些被称为副作用side effects。如果测试中发现一个代码单元(unit of code)中有很多side effects需要测试,可能需要考虑SOLID原则中Single responsiblity principle了。采用这个原则表明代码单元中包括的东西太多,最好重构(refactored)/拆分。符合single responsibility原则可保证代码的可重复性和可靠性。
优先级:side_effect
> return_value
,即当这两个参数同时指定时,显示side_effect
的(迭代)结果。
>>> tmp3 = Mock(return_value=99, side_effect=[148, 149, 150])
>>> tmp3(1)
148
>>> tmp3(2)
149
>>> tmp3(3)
150
>>> tmp3(4)
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1093, in __call__
return self._mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1154, in _execute_mock_call
result = next(effect)
StopIteration
观察上面的结果可以看到,当side_effect
可迭代对象中的元素被遍历完成时,再次调用Mock
实例并赋值则返回StopIteration
。该功能可用于模拟同一个函数被多次调用,且可以返回不同的值或相同的值,由使用者定义,比如模拟用户发送请求给web服务器得到的反馈等。
side_effect
可用于引发异常,实例化时给side_effect
赋值为异常则在调用时返回指定的异常。
>>> tmp4 = Mock(side_effect=[Exception('1'), Exception('2')])
>>> tmp4(1)
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1093, in __call__
return self._mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1156, in _execute_mock_call
raise result
Exception: 1
>>> tmp4(00)
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1093, in __call__
return self._mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1156, in _execute_mock_call
raise result
Exception: 2
如何循环返回side_effect
中的对象?
如果side_effect
指定为特定的对象而非可迭代对象,则每次调用都返回这个指定的结果。
>>> tmp7 = Mock(side_effect=Exception('1'))
>>> tmp7(1)
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1093, in __call__
return self._mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1152, in _execute_mock_call
raise effect
Exception: 1
>>> tmp7(2)
Traceback (most recent call last):
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1093, in __call__
return self._mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1152, in _execute_mock_call
raise effect
File "", line 1, in
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1093, in __call__
return self._mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1097, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "/Users/jeffcheung/opt/anaconda3/lib/python3.9/unittest/mock.py", line 1152, in _execute_mock_call
raise effect
Exception: 1
如何循环返回可迭代对象?
placeholder
mock.MagicMock
(2023.04.02 Sun)
MagicMock对象提供了简单的模拟接口,允许用户设定对象返回值,或被patch对象的其他行为。允许用户完全定义调用的行为,避免创建真实对象。比如patch一个对requests.get
的调用,采用HTTP library调用,通过MagicMock可以定义响应,而不必强制服务器返回想要得到的响应。
MagicMock的return_value也可以是MagicMock,而前面的一般Mock
,其返回值可以为多种形式,除了MagicMock
。
MagicMock对于模拟那些带有复杂要求的类提供了灵活性和便利性,但是因为其过于灵活,也会带来一些问题。默认情况下,MagicMock有任何用户需要的属性(attribute),甚至用户不希望其拥有的属性也可以拥有。但有时需要对MagicMock的返回做出限制,比如需要一个MagicMock返回Response对象而非Request对象,但此时因为MagicMock的灵活性,仍然会返回Request对象。这将导致测试失败。
针对这个问题,MagicMock在初始化时可用spec
参数指定输入关键词,如MagicMock(spec=Response)
或MagicMock(spec=class_a)
。这种方式创建的MagicMock对象其中只含有Response
或class_a
拥有的属性。一旦用户访问的属性不在spec
指定的对象中,对该MagicMock的访问将返回错误。这个方法成为speccing。参考下面案例
>> class ca:
... def a(self):
... pass
... def b(self):
... pass
...
>> m = MagicMock(spec=ca)
>> m.a.return_value=998
>> m.b.return_value=198
>> m.a()
998
>> m.c()
Traceback (most recent call last):
File "", line 1, in
File "C:\ProgramData\Anaconda3\lib\unittest\mock.py", line 637, in __getattr__
raise AttributeError("Mock object has no attribute %r" % name)
AttributeError: Mock object has no attribute 'c'
定义了一个类ca
,其中的方法只有a
和b
。在定义MagicMock m
时指定spec=ca
。一旦访问m
中除a
和b
以外的属性,则返回AttributeError
。
除了用于指定MagicMock对象的属性,用speccing还可以用于只能匹配。考虑下面的MagicMock对象,其中spec指定为一个三个输入参数的函数,注意观察该MagicMock对象在判断其assert_called_with
时的返回结果。
>> def f(a,b,c):
... return 'd'
>> m = MagicMock(spec=f)
>> m(1, 9, 8)
>> m.assert_called_with(a=1, b=9, c=8)
>> m.assert_called_with(a=2,b=9,c=8)
Traceback (most recent call last):
File "", line 1, in
File "/usr/lib/python3.8/unittest/mock.py", line 913, in assert_called_with
raise AssertionError(_error_message()) from cause
AssertionError: expected call not found.
Expected: mock(a=2, b=9, c=8)
Actual: mock(1, 9, 8)
实例
(2022.12.19 Mon)
实例化unittest.mock
中的Mock
类,并将该实例赋给被代替/模拟的函数,其中Mock
实例化时的return_value
值就是被模拟/代替函数的返回值。
from unittest.mock import Mock
def hello_x(s):
print(f"halo {s}")
>> m = Mock(return_value="no hello world")
>> hello_x = x
>> hello_x('1234')
'no hello world'
Mock
对象中return_value
的作用在于忽略mock对象的行为,指定其返回值。注意,Mock
首先实例化,再赋给方法,放在在使用时仍然需要赋值。
再看一个在unittest
中的案例。
modular.py中的类
class Count():
def add(self):
pass
在Unittest文件中
from unittest import mock
import unittest
from modular import Count
# test Count class
class TestCount(unittest.TestCase):
def test_add(self):
count = Count()
count.add = mock.Mock(return_value=13)
result = count.add(8,5)
self.assertEqual(result,13)
if __name__ == '__main__':
unittest.main()
观察该案例中,Count
类的add
方法并没有实现方案,相当于忽略了该方法实现的细节。而只借助mock
返回该方法的结果,即count.add = mock.Mock(return_value=13)
。调用count.add
方法时,不论Count.add
如何实现,返回结果都是return_value=13
。
注意到如果在用Mock
对方代替某个方法且未指定return_value
时,则默认的return_value
是一个Mock
对象。
>> a = Mock()
>> a.return_value
>> type(a.return_value)
设置Mock
(2023.01.15 Sun)
上面部分看到一个Mock
实例有return_value
和side_effects
等参数,通过Mock
的configure_mock
方法可一次性将这些参数赋给实例。
>>> cm = Mock()
>>> cm.configure_mock(name='con_mock',return_value=100, side_effect=['a', 'b'])
>>> cm(1)
'a'
>>> cm(2)
'b'
>>> cm1 = Mock()
>>> cm1.configure_mock(name='con_mock', return_value=198)
>>> cm1(199)
198
mock.patch
和mock.patch.object
(2023.02.11 Sat)
Mock提供了patch
方法用于代替特定对象中的对象/方法。
使用方法包括装饰器和context manager,以装饰器为主。decorator有如下两个用法
@patch("
. ") @patch.object(module_name/class_name, method_name)
patch作为装饰器,被模拟的方法写在method_name
的位置,在其后的unit test中为它赋实例,再用return_value
来指定模拟函数的返回结果。例如下面的文本为send_demo.py
import requests
def send_request(url):
r = requests.get(url)
return r.status_code
def visit_xxx():
url = 'http://www.xxx.com'
return send_request(url)
写一个test case来指定其中的send_request
的返回结果
from unittest import mock
import unittest
import send_demo
class TestReq(unittest.TestCase):
@mock.patch("demo.send_request")
def test_request_200(self,mock_request):
mock_request.return_value='200' # 设定return_value
self.assertEqual(demo.visit_xxx(), '200')
@mock.patch("demo.send_request")
def test_request_404(self,mock_request):
mock_request.return_value='404' # 设定return value
self.assertEqual(demo.visit_xxx(), '404')
mock.patch
的几个潜在问题和应对策略
(2023.04.02 Sun@HRB)
tips: patch
常用于patch外部API调用,或者其他的耗时函数(time-intensive function)或资源消耗性函数(resource-intensive function),或对象。每个测试不宜patch太多,如果发现每个测试中patch过多,则需要考虑重构测试或函数。
多个patch装饰器和测试函数的输入变量的顺序
使用patch装饰器将会自动传递位置变量(positional argument)给被装饰的测试函数。当多个patch装饰器装饰测试函数,则距离测试函数最近的装饰器首先调用并创建第一个位置变量。
比如
@mock.patch("module.classB")
@mock.patch("module.functionA")
def test_case(self, mock_A, mock_B):
pass
在测试函数test_case
中,最先传递的参数mock_A
是patch装饰器中离该函数最近的装饰器@mock.patch("module.functionA")
,之后以此类推。
patch装饰器的类型
默认情况下,patch的输入函数是MagicMock实例,也就是unittest.mock的默认模拟对象。可通过修改MagicMock实例的返回类型以定义被patch函数的行为,
patch
对象的作用域(scope)
patch被用作测试函数的装饰器,其输入变量是一个字符串,该字符串代表了被patch的对象(函数或类)。同时需要指出被patch对象的完整路径/名称,方便对该对象定位。注意,一旦某个类Class A
在文件main.py
中被命令from module import ClassA
引入,则Class A
成为引入它的模块的命名空间的一部分,即是main.py
的一部分而非module
的一部分。
比如一段代码在脚本module_a.py
中
# module_a.py
from module_b import classA
...
在相应的测试代码中,对classA
的patch操作,不可写成@patch("module_b.classA")
尽管classA
来自module_b
,而写成其被导入的地址
@mock.patch("module_a.classA")
因from module_b import classA
命令将classA
引入到当前的命名空间(namespace)中。
如何用mock
模拟全局变量(global variable)和环境变量(environmental variable)
(2023.03.22 Wed)
用mock模拟代码中的全局变量,用到mock.patch
中的new
参数,将全局变量的值赋给new
参数,即可设置全局变量。
有代码脚本main.py
# main.py
gv = 100
class testClass():
global gv
pass
def test_function():
global gv
return gv+1
在测试脚本test.py
中,用mock.patch("main.gv", new=101)
这样的命令即可模拟global variable。
import unittest
from unittest import mock
import main
def testCase(unittest.TestCase):
@mock.patch("main.gv", new=101)
def test_1(self):
new_gv = 102
func = main.test_function()
self.assertEqual(func, new_gv)
运行结果如下
>> pytest test_main.py
============================= test session starts ==============================
platform darwin -- Python 3.9.7, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/directory
plugins: anyio-3.6.2
collected 1 item
test_main.py . [100%]
============================== 1 passed in 0.02s ===============================
特别注意,先运行的test case,其global variable会被后面的test cases使用。为免于影响,可对每个test case的global variable做装饰,并用new赋值。
下面函数用于返回环境变量PATH
,在测试中mock环境变量。
# main_env.py
import os
def output_env():
return os.environ["PATH"]
测试代码
# test_env.py
import os
import unittest
from unittest import mock
import main_env
class testCase(unittest.TestCase):
@mock.patch.dict(os.environ, {"PATH": f"test_path"})
def test_1(self):
expected_path = "test_path"
tmp = main_env.output_env()
self.assertEqual(tmp, expected_path)
运行测试案例,结果如下
>> pytest test_env.py
============================= test session starts ==============================
platform darwin -- Python 3.9.7, pytest-7.1.2, pluggy-1.0.0
rootdir: /Users/codes
plugins: anyio-3.6.2
collected 1 item
test_env.py . [100%]
============================== 1 passed in 0.02s ===============================
Reference
1 realpython, Understanding the Python Mock Object Library, Alex Ronquillo