UnitTest 子包 Mock

🍰 Mock模块是Python测试框架UnitTest下的一个子包,是用于模拟对象行为的工具,也是单元测试的一个重要模块。

1 对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from unittest.mock import MagicMock

obj = MagicMock()
print(dir(obj))

# 构造器
# __init__(name=None, spec=None, return_value=DEFAULT, side_effect=None)
#
# 断言方法
# assert_called()
# assert_not_called()
# assert_called_once()
# assert_any_call(*args, **kwargs)
# assert_has_calls(calls, any_order)
# assert_called_with(*args, **kwargs)
# assert_called_once_with(*args, **kwargs)
#
# 管理方法
# reset_mock()
# attach_mock(mock, attribute)
# mock_add_spec(spec, spec_set=False)
# configure_mock(**kwargs)
#
# 统计方法
# called: Boolean
# call_count: integer
# call_args: tuple
# call_args_list: list
# mock_calls: list
# method_calls: list

2 构造器

  • 构造器
    • init是mock对象的构造器,name是mock对象的唯一标识。
    • spec设置的是mock对象的属性,可以是属性、方法、列表字符串或类。
    • 当这个mock对象被调用时候显示出的结果,即return_value设置的值。
    • side_effect会覆盖return_value,当mock对象被调用时返回side_effect的值,而非return_value。

2-1 name

1
2
3
4
5
6
7
8
9
10
from unittest.mock import Mock

# name标识了唯一的一个mock
mock_obj = Mock(name="ok")
print(mock_obj)
# repr(object)方法返回对象的字符串形式
print(repr(mock_obj))

# <Mock name='ok' id='2364967632848'>
# <Mock name='ok' id='2364967632848'>

2-2 spec:list

1
2
3
4
5
6
7
8
9
10
11
12
13
from unittest.mock import Mock

list = ["a", "b", "c"]
# spec指定的是属性组成的list
mock_foo = Mock(spec=list)

print(mock_foo)
print(mock_foo.a)
print(mock_foo.c)

# <Mock id='1957190523440'>
# <Mock name='mock.a' id='1957190522288'>
# <Mock name='mock.c' id='1957190521856'>

2-3 spec:class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from unittest.mock import Mock


# People类有三个属性,一个变量,两个方法
class People:
_num = 24

def f1(self):
print("f1()")

def f2(self, value):
print("value = ", value)


# spec指定的是一个类属性
mock_foo = Mock(spec=People)

print(mock_foo)
print(mock_foo._num)
print(mock_foo.f1())
print(mock_foo.f2())

# <Mock spec='People' id='1486312582256'>
# <Mock name='mock._num' id='1486316220224'>
# <Mock name='mock.f1()' id='1486346522576'>
# <Mock name='mock.f2()' id='1486318466576'>

2-4 return_value:value

1
2
3
4
5
6
7
8
9
10
11
from unittest.mock import Mock

# 使用return_value指定一个值
mock_foo = Mock(return_value=24)
print(mock_foo)
# 当调用mock对象时,显示的就是return_value的值
mock_obj = mock_foo()
print(mock_obj)

# <Mock id='2159666551344'>
# 24

2-5 return_value:object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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指定一个对象
mock_p = Mock(return_value=p)
print(mock_p)

# 调用mock对象,会返回上面设置的对象p
mock_obj = mock_p()
print(mock_obj)

# 调用mock对象p的属性和方法
print(mock_obj._num)
print(mock_obj.f1())
# 调用方法也需要传入参数
print(mock_obj.f2(8))

# <__main__.People object at 0x00000245244F2C70>
# <Mock id='2495988801248'>
# <__main__.People object at 0x00000245244F2C70>
# 24
# f1()
# None
# value = 8
# None

2-6 side_effect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

# 每执行一次,会返回list中的一个值,返回完之后就会抛出异常
# mock_obj = mock_foo()
# print(mock_obj)

# 1
# 2
# 3

3 断言方法

  • 断言方法
    • assert_called()·······························检查模拟对象是否至少被调用一次
    • assert_not_called()···························检查模拟对象是否没有被调用
    • assert_called_once()··························检查模拟对象是否仅被调用了一次
    • assert_any_call(*args, **kwargs)··············检查模拟对象在测试例程中是否调用了方法
    • assert_has_calls(calls, any_order)············检查是否按照正确顺序和正确参数调用方法
    • assert_called_with(*args, **kwargs)···········检查模拟对象是否获得了正确的参数
    • assert_called_once_with(*args, **kwargs)······检查模拟对象是否仅调用了一次方法

3-1 assert_called

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()
mock_foo.f2()
mock_foo.f2(5)
mock_foo.f2.assert_called()

3-2 assert_not_called

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.f2.assert_not_called()

3-3 assert_called_once

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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_once()
mock_foo.f2(5)
mock_foo.f2.assert_called_once()

3-4 assert_any_call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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_any_call()
mock_foo.f2(5)
mock_foo.f2.assert_any_call(5)

3-5 assert_has_calls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from unittest.mock import Mock, call


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.f2()
mock_foo.f2(5)
# 按照上面的调用顺序进行断言,断言成功
foo_calls = [call.f1(), call.f2(), call.f2(5)]
mock_foo.assert_has_calls(foo_calls)

3-6 assert_called_with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)

# 调用方法,检查模拟对象是否获得了正确的参数
# 当至少有一个参数带错误值或类型时
# 当参数的个数出错时
# 当参数的顺序不正确时
# assert_called_with断言就会发生错误
# 方法f1()不需要参数
mock_foo.f1()
mock_foo.f1.assert_called_with()
# 方法f2()需要一个参数,不传参会断言失败,报错显示
# mock_foo.f2()
# mock_foo.f2.assert_called_with(5)

3-7 assert_called_once_with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

# 调用方法,检查模拟对象是否仅调用了一次方法
# 当某个方法被多次调用时,assert_called_once_with断言就会报错
mock_foo.f2(10)
mock_foo.f2.assert_called_once_with(10)
# 方法f2()被调用第2次,断言失败,报错显示
# mock_foo.f2(11)
# mock_foo.f2.assert_called_once_with(11)

4 管理方法

  • 管理方法
    • reset_mock()··························重置mock对象的状态,包括调用情况和属性设置
    • attach_mock(mock, attribute)··········将另一个mock对象作为属性附加到当前mock对象上
    • mock_add_spec(spec, spec_set=False)···将另一个对象的属性作为规范添加到当前mock对象上
    • configure_mock(**kwargs)··············配置mock对象的属性,通过关键字参数指定属性及其值

4-1 reset_mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
mock_foo.f1()
mock_foo.f2(3)

# 重置mock对象的状态,断言mock对象从未被调用过
mock_foo.reset_mock()
mock_foo.f1.assert_not_called()
mock_foo.f2.assert_not_called()

4-2 attach_mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from unittest.mock import Mock


class People1:
_num = 24

def f1(self):
print("f1()")

def f2(self, value):
print("value = ", value)


class People2:
_num = 25

def f3(self):
print("f3()")

def f4(self, value):
print("value = ", value)


mock_foo1 = Mock(spec=People1)
mock_foo2 = Mock(spec=People2)
print(mock_foo1)
print(mock_foo2)

# 将mock_foo2对象作为属性附加到当前的mock_foo1对象上
mock_foo1.attach_mock(mock_foo2, "attach_mock")
print(mock_foo1)
print(mock_foo1._num)
print(mock_foo1.f1())
print(mock_foo1.f2(3))
print(mock_foo1.attach_mock)
print(mock_foo1.attach_mock._num)
print(mock_foo1.attach_mock.f3())
print(mock_foo1.attach_mock.f4(5))

4-3 mock_add_spec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from unittest.mock import Mock


class People:
_num = 24

# 类方法,self参数代表当前类的实例,通过self来访问实例的属性和方法
def f1(self):
print("f1()")

def f2(self, value):
print("value = ", value)


# 普通函数,不属于任何类,也不需要传入self参数,可以直接被调用
def f3():
print("f3()")


class Add:
_num = 25

def f4(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(3))

# 将f3()对象的属性作为规范添加到当前mock_foo对象上,mock_foo对象原来的属性会被擦除
# spec_set参数指属性可读可写,默认只读,如果想让其拥有写的权限,可以设置为True
mock_foo.mock_add_spec(f3)
print(mock_foo)

mock_foo.mock_add_spec(Add)
print(mock_foo._num)
print(mock_foo.f4())

4-4 configure_mock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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, return_value=123)
print(mock_foo())
print(mock_foo._num)
print(mock_foo.f1())
print(mock_foo.f2(3))

# 修改return_value的值
mock_foo.configure_mock(return_value=456)
print(mock_foo())

soo_spec = {"f1.return_value": 100, "f2.return_value": 200, "f2.side_effect": [300, 500]}
mock_foo.configure_mock(**soo_spec)
print(mock_foo.f1())
print(mock_foo.f2())

5 统计方法

  • 统计方法
    • called: Boolean············mock对象是否曾被调用
    • call_count: integer········mock对象被调用的次数
    • call_args: tuple···········mock对象最近一次调用时的参数
    • call_args_list: list·······mock对象所有调用时的参数列表
    • mock_calls: list···········mock对象的所有调用情况
    • method_calls: list·········mock对象的所有方法调用情况

5-1 called

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
# mock对象是否曾被调用
print(mock_foo.called)
# 调用后,再次验证
mock_foo()
print(mock_foo.called)

5-2 call_count

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
mock_foo()
mock_foo()
mock_foo()
# mock对象被调用的次数
print(mock_foo.call_count)

5-3 call_args

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)
mock_foo()
# mock对象最近一次调用时的参数
print(mock_foo.call_args)

5-4 call_args_list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
mock_foo()
mock_foo()._num()
mock_foo().f1()
mock_foo().f2(3)
# mock对象所有调用时的参数列表
print(mock_foo.call_args_list)

5-5 mock_calls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
mock_foo()
mock_foo._num()
mock_foo.f1()
mock_foo.f2(3)
# mock对象的所有调用情况
print(mock_foo.mock_calls)

5-6 method_calls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
mock_foo()
mock_foo._num()
mock_foo.f1()
mock_foo.f2(3)
# mock对象的所有方法调用情况
print(mock_foo.method_calls)

6 mock示例

1
2
3
4
5
6
7
8
9
10
11
12
13
logs:                               # 存储项目日志,需要事先手动创建该文件夹
- test_log.log # 测试输出日志
- student_log.log
common: # 存储公用方法
- __init__.py # 实现单例模式
- log_output.py # 日志功能封装
reports: # 存储项目报告,需要事先手动创建该文件夹
- test_beautifulreport.html
test_cases: # 存储测试用例
- test_student.py
- main.py # 作为入口函数
- student.py # 学生类模块
- settings.py # 配置文件信息

6-1 main.py

1
2
3
4
5
6
7
8
9
10
11
# main.py,项目入口
import unittest
import settings
from BeautifulReport import BeautifulReport

if __name__ == "__main__":
# 收集所有的用例,并返回测试套件
suite = unittest.TestLoader().discover("test_cases")

# 执行所有的用例,并生成报告
BeautifulReport(suite).report(**settings.REPORT_CONFIG)

6-2 student.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# student.py,定义一个Student类
class Student:
def __init__(self):
self.__age = 20

# 定义两个成员方法,一个带参,一个无参
def get_name(self, first_name, last_name):
return first_name + " " + last_name

def get_age(self):
return self.__age

# 定义一个静态方法
@staticmethod
def get_class_name():
return Student.__name__

6-3 settings.py

1
2
3
4
5
6
7
8
9
10
11
12
# settings.py,日志配置
LOG_CONFIG = {
"name": "student_log",
"filename": "logs/student_log.log",
"debug": True
}

# 测试报告
REPORT_CONFIG = {
"filename": "reports/test_beautifulreport.html",
"description": "UnitTest结合Mock测试"
}

6-4 __init__.py

1
2
3
4
5
6
# __init__.py
import settings
from .log_output import get_logger

# 单例模式,将settings.LOG_CONFIG的值解包传入
logger = get_logger(**settings.LOG_CONFIG)

6-5 log_output.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# log_output.py
import logging


def get_logger(name, filename, mode="a", encoding="utf-8", fmt=None, debug=False):
"""
:param name: 日志记录器的名称
:param filename: 日志文件名
:param mode: 文件模式
:param encoding: 字符编码
:param fmt: 日志格式
:param debug: 调试模式
:return:
"""
# 创建一个日志记录器并设置日志等级
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)

# 确定日志文件和控制台输出的日志级别,文件处理器的等级一般情况下比控制台要高
if debug:
file_level = logging.DEBUG
console_level = logging.DEBUG
else:
file_level = logging.WARNING
console_level = logging.INFO

# 定义日志的输出格式
if fmt is None:
fmt = "%(asctime)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] " \
"%(levelname)s - %(message)s"

# 创建日志处理器,写入文件中并设置日志等级
file_handler = logging.FileHandler(filename=filename, mode=mode, encoding=encoding)
file_handler.setLevel(file_level)

# 写入控制台的日志处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(console_level)

# 创建格式化器并添加到日志处理器
formatter = logging.Formatter(fmt=fmt)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

# 将日志处理器添加到日志器上
logger.addHandler(file_handler)
logger.addHandler(console_handler)

# 返回日志
return logger


if __name__ == "__main__":
# logs文件夹需要手动创建,否则报错
logger = get_logger(name="test", filename="../logs/test_log.log", debug=True)
logger.debug("调试日志")
logger.info("普通日志")
logger.warning("警告日志")
logger.error("错误日志")
logger.critical("严重错误")

6-6 test_student.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# test_student.py
import unittest
from common import logger
from student import Student
from unittest.mock import Mock, create_autospec


class StudentTest(unittest.TestCase):
# 将日志相关内容定义为类属性
logger = logger
counter = 0

@classmethod
def setUpClass(cls):
cls.logger.info("Mock测试开始")

@classmethod
def tearDownClass(cls):
cls.logger.info("Mock测试结束")

def setUp(self):
StudentTest.counter += 1
# print(f"开始执行第{StudentTest.counter}个用例")
self.logger.info(f"用例-{StudentTest.counter}开始测试")

def tearDown(self):
# print(f"结束执行第{StudentTest.counter}个用例")
self.logger.info(f"用例-{StudentTest.counter}结束测试")

# 使用Mock类的return_value指定返回值
def test_get_name_1(self):
s = Student()

# 正常调用get_name()
print(s.get_name("刘", "一"))

# mock掉get_name(),使用return_value指定返回值,让其返回陈二
s.get_name = Mock(return_value="陈二")

# 调用s.get_name()时没有给定任何参数,但依然可以工作,并且任意指定参数对结果没影响
# 说明以下方式无法校验参数,若想校验参数,需要使用create_autospec模块方法替代Mock类
self.assertEqual(s.get_name(), "陈二")
self.assertEqual(s.get_name("1"), "陈二")
self.assertEqual(s.get_name("1", "2"), "陈二")
self.assertEqual(s.get_name("1", "2", "3"), "陈二")

# 使用create_autospec模块方法替代Mock类
def test_get_name_2(self):
# 创建一个自动模拟Student类的实例
s = create_autospec(Student)

# 断言调用get_name()方法时的参数,未调用时的参数校验
self.assertEqual(s.get_name.call_count, 0)
print(s.get_name("陈", "二"))
# 调用后的参数校验
self.assertEqual(s.get_name.call_count, 1)
s.get_name.assert_called_with("陈", "二")

# 校验参数个数,再返回固定值
def test_get_name_3(self):
s = Student()

# 任意给定两个参数,依然会返回mock的值,但参数个数必须对
s.get_name = create_autospec(s.get_name, return_value="张 三")
# 若参数个数不对,将报错TypeError: missing a required argument: "first_name"
self.assertEqual(s.get_name("1", "2"), "张 三")

# 根据不同的参数,返回不同的值
def test_get_name_4(self):
s = Student()

values = {("李", "四"): "李 四", ("王", "五"): "王 五"}
s.get_name = Mock(side_effect=lambda x, y: values[(x, y)])
self.assertEqual(s.get_name("李", "四"), "李 四")
self.assertEqual(s.get_name("王", "五"), "王 五")

# 使用Mock类的return_value指定返回值
def test_get_age_1(self):
s = Student()

# 不使用mock,正常调用get_age()
self.assertEqual(s.get_age(), 20)
# mock掉get_age(),使用return_value指定返回值,让其返回21
s.get_age = Mock(return_value=21)
self.assertEqual(s.get_age(), 21)

# 使用side_effect,依次返回指定值
def test_get_age_2(self):
s = Student()

# 指定一组返回值,side_effect会覆盖return_value的值
list = [27, 28, 29]
s.get_age = Mock(return_value=20, side_effect=list)
self.assertEqual(s.get_age(), 27)
self.assertEqual(s.get_age(), 28)
self.assertEqual(s.get_age(), 29)

# 异常用例,确保代码在面对异常情况时能够正确地处理和报告异常,提高代码的健壮性和可靠性
# def test_raise_exception(self):
# s = Student()
#
# s.get_age = Mock(side_effect=TypeError("integer type"))
# # 只要调用get_age()方法,就会抛出异常
# self.assertRaises(TypeError, s.get_age())

UnitTest 子包 Mock
https://stitch-top.github.io/2024/03/02/ce-shi-kuang-jia/tf05-unittest-zi-bao-mock/
作者
Dr.626
发布于
2024年3月2日 23:07:12
许可协议