Python测试-unittest mock, 2022-12-18

(2022.12.18 Sun)
Mock简单来理解,就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试。而这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。

Python 3.3版本之前,mock是个独立模块,之后被整合进入python的unittest模块中。

Mock基本用法

(2023.01.08 Sun)
unittest.mock提供了用于模拟对象的基类MockMock使用相当灵活,其用例相当广泛。

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_valueside_effect

(2023.01.12 Thur)
Mock类在实例化时可以针对其中的return_valueside_effect两个参数。

  • return_valueMock实例的返回值,指定一个对象,当Mock实例被调用,则返回其指定的值
  • side_effectMock实例的异常或动态改变值,可以是一个值,或一个可迭代序列对象。当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对象其中只含有Responseclass_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,其中的方法只有ab。在定义MagicMock m时指定spec=ca。一旦访问m中除ab以外的属性,则返回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_valueside_effects等参数,通过Mockconfigure_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.patchmock.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

你可能感兴趣的:(Python测试-unittest mock, 2022-12-18)