文章目录
- [1. 单元测试用例目录](#1. 单元测试用例目录)
- [2. 自动化测试用例编写步骤](#2. 自动化测试用例编写步骤)
- [3. 命名规则](#3. 命名规则)
- [4. 环境安装](#4. 环境安装)
- [5. pytest语法](#5. pytest语法)
-
- [5.1 unittest与pytest对比](#5.1 unittest与pytest对比)
- [5.2 pytest运行插件](#5.2 pytest运行插件)
- [5.3 fixture](#5.3 fixture)
- [5.4 装饰器](#5.4 装饰器)
- [6. pytest.ini](#6. pytest.ini)
- [7. conftest.py](#7. conftest.py)
- [8. 用例编写步骤](#8. 用例编写步骤)
-
- [8.1 按照以下方式检查用例](#8.1 按照以下方式检查用例)
- [9. 单元测试示例](#9. 单元测试示例)
- [10. 运行](#10. 运行)
- [11. 覆盖率](#11. 覆盖率)
- [12. 单个文件覆盖率详情](#12. 单个文件覆盖率详情)
- [14. 测试报告](#14. 测试报告)
- 15.Mock
-
- [15.1 什么是Mock](#15.1 什么是Mock)
- [15.2 Mock的定义](#15.2 Mock的定义)
- [15.3 Mock的使用](#15.3 Mock的使用)
- [15.4 Mock与MagicMock](#15.4 Mock与MagicMock)
- [15.5 Mock示例](#15.5 Mock示例)
- [16. MagicMock](#16. MagicMock)
- 17.Patch
-
- [17.1 patch能做什么](#17.1 patch能做什么)
- [17.2 patch的目标](#17.2 patch的目标)
- [17.3 patch定义](#17.3 patch定义)
- [17.3 patch的使用方式](#17.3 patch的使用方式)
-
- [17.3.1 装饰器方式](#17.3.1 装饰器方式)
- [17.3.2 上下文管理器](#17.3.2 上下文管理器)
- [17.3.3 手动方式](#17.3.3 手动方式)
1. 单元测试用例目录
- testPytest
- business
- .coveragerc # 覆盖率配置文件
- pytest.ini # pytest测试框架配置文件,运行参数
- run_unittest.py # 运行入口
- tests
- assets # README.md图片目录
- common # 公共方法
- data # 单元测试用例数据目录
- env-pytest3.6 # 测试工具环境
- example # 示例
- logs # 日志
- business # 测试用例模块目录
- conftest.py # 自定义hook程序
- install_env.sh # python单元测试环境安装
- README.md # 单元测试文档
- requirements.txt # 单元测试工具安装
2. 自动化测试用例编写步骤
python
复制代码
1. 初始化 - 用例执行前的动作
2. 执行 - 具体用例逻辑
3. 断言 - 校验用例执行结果
4. 清理 - 用例执行后的动作 - 一般是把用例修改的内容恢复到执行前的状态
3. 命名规则
python
复制代码
pytest的命名规则:
1. 模块名(py文件)必须是以 test_ 开头或者 _test 结尾;
2. 测试类(class)必须是以 Test 开头,并且不能带 __init__ 方法;
3. 测试用例 - 类方法(method)必须是以 test_ 开头;
4. 测试用例 - 函数(function)必须是以 test_ 开头;
4. 环境安装
python
复制代码
pip3 install env-pytest3.6
# 或者
pip3 install -r requirements.txt
- env-pytest3.6:管软kos环境适配whl包
pytest
复制代码
allure_pytest-2.9.45-py3-none-any.whl
allure_python_commons-2.9.45-py3-none-any.whl
atomicwrites-1.4.0-py2.py3-none-any.whl
attrs-21.4.0-py2.py3-none-any.whl
colorama-0.4.4-py2.py3-none-any.whl
importlib_metadata-4.8.3-py3-none-any.whl
iniconfig-1.1.1-py2.py3-none-any.whl
packaging-21.3-py3-none-any.whl
pluggy-1.0.0-py2.py3-none-any.whl
py-1.11.0-py2.py3-none-any.whl
pyparsing-3.0.7-py3-none-any.whl
pytest-7.0.1-py3-none-any.whl
pytest_assume-2.4.3-py3-none-any.whl
pytest_html-3.1.1-py3-none-any.whl
pytest_metadata-1.11.0-py2.py3-none-any.whl
pytest_repeat-0.9.1-py2.py3-none-any.whl
pytest_rerunfailures-10.2-py3-none-any.whl
setuptools-59.6.0-py3-none-any.whl
six-1.16.0-py2.py3-none-any.whl
tomli-1.2.3-py3-none-any.whl
typing_extensions-4.1.1-py3-none-any.whl
zipp-3.6.0-py3-none-any.whl
- requirements.txt:python3.6版本单元测试包和版本
pytest
复制代码
allure_pytest==2.9.45
allure_python_commons==2.9.45
atomicwrites==1.4.0
attrs==21.4.0
colorama==0.4.4
importlib_metadata==4.8.3
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.7
pytest==7.0.1
pytest_assume==2.4.3
pytest_html==3.1.1
pytest_metadata==1.11.0
pytest_repeat==0.9.1
pytest_rerunfailures==10.2
setuptools==59.6.0
six==1.16.0
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0
5. pytest语法
5.1 unittest与pytest对比
5.2 pytest运行插件
5.3 fixture
python
复制代码
# 函数格式:
fixture(scope,autouse,params,ids,name):
scope: 在什么层级下运行,它的值只有:session, package, module, class, function(默认值)
autouse: 代表的是否自动执行,若此参数不加或者设置为False的话,代表不会自动运行,设置为True自动执行,无需调用。
params: 进行的数据参数化,参数化的数据就是通过此参数传入到测试用例中。
ids: 它是给生成的结果的fixture进行重命名,主要是为了好理解,前提是必须要有参数params。
name: 对fixture的函数名起别名,或者叫重命名,没啥大用处。
# 参数名:scope
session:如果等于此值,那么这个fixture在整个项目下只运行一次,这个特别适合于登录,登录在项目中只需要登录一次。
package:如果设置为此值,那么这个fixture在这个包下只运行一次。
module:如果设置为此值,那么这个fixture在这个文件中只运行一次,文件中可能既有函数又有类。
class:如果设置为此值,那么这个fixture在这个类中只运行一次。
function:它是这个函数的默认值,如果为此值,那么这个fixture在每个测试函数前运行一次。
# 参数名:autouse
True: 如果等于此值,此fixture将被自动调用,而且都是在测试用例前执行。
False:该值是此参数的默认值,如果等于此值,此fixture将不会自动调用,需要主动调用。
# 参数名:params
它的值主要接受的是列表,而列表中的值存放的就是测试数据
# 参数名:ids
它是给生成的结fixture进行重命名,因为它之前生成的模式是按照fixture_demo[index] 进行命名的,其中这个index就是每次循环的数字,第一次为0 。如果这个不太好理解,就可以使用ids进行重命名,但是前提必须要有参数化:params。
# 参数名:name
它是给fixture函数重名名的,或者叫起别名,主要是为了调用时方便使用,一旦起别名,在测试用例中就必须的使用别名。
5.4 装饰器
6. pytest.ini
python
复制代码
# pytest.ini是什么?
pytest.ini文件是pytest的主配置文件;
pytest.ini文件的位置一般放在项目的根目录下,不能随便放,也不能更改名字。
pytest.ini文件中都是存放的一些配置选项,这些选项都可以通过pytest -h查看到。
# pytest.ini的编写格式
INI文件由节、键、值组成。
节 [section]
选项/参数(键=值) name=value
注释 注释使用分号表示(;)。在分号后面的文字,直到该行结尾都全部为注释。
ini
复制代码
# 例如:
[pytest]
;命令参数
addopts =
-v ; 输出更加详细的信息
-s ; 输出测试用例的打印信息,比如在用例中有print信息
--reruns 3 ; 失败用例重跑,最多重试3次
--reruns-delay 3 ; 重试前等待秒数
--html=unittest_report.html ; 生成HTML报告
--self-contained-html ; 合并CSS的HTML报告,分享报告样式不丢失
--capture=sys ; 控制测试用例执行过程输出,sys表示捕获Python层级的sys.stdout和sys.stderr
--cov=business
--cov-branch
--cov-config .coveragerc
--cov-report=html:unittest_coverage_html
--cov-report=xml:unittest_coverage.xml
;指定测试用例跟目录,只有在pytest未指定文件目录或测试用例标识符时,该选项才有作用
;testpaths = ./tests
;屏蔽不被检索单元测试用例的目录
;norecursedirs = .git
;屏蔽告警
filterwarnings = ignore::DeprecationWarning
asyncio_default_fixture_loop_scope = function
asyncio_mode = auto
;解决测试用例参数话时ids中文乱码
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
;日志设置
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s - %(name)s - (%(levelname)s) - %(filename)s - %(lineno)d - %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
log_format = %(asctime)s - %(name)s - (%(levelname)s) - %(filename)s - %(lineno)d - %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
;测试用例匹配规则
python_files = test_*.py
python_classes = Test*
python_functions = test_*
;注册自定义标签
markers =
nas: file storage
dos: object storage
san: block storage
mix: unstructured storage
smoke: smoke test
python
复制代码
# conftest.py是什么?
conftest.py是fixture函数的一个集合,将一些前置动作放在fixture中,然后供其它测试用例调用。不同于普通被调用的模块,conftest.py使用时不需要导入,Pytest会首先查找这个文件。
# conftest.py有什么作用?
调用系统之前你需要先做登录操作,获取token
执行测试用例前先需要搜集测试用例数据。
执行测试用例前先需要进行筛选数据等。
# conftest.py使用规则
它的文件名是固定的,不可更改。
它可以放在项目的跟目录下,也可以放在每个测试用例的文件夹下。
每次执行时都是首先先运行此文件下的fixture函数,然后才去执行测试用例文件。
# 示例参考:tests/conftest.py
python
复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import pytest
import datetime
from py.xml import html
from pytest_metadata.plugin import metadata_key
"""
自定义hook程序
"""
# 定义一个全局变量,用于存储内容
global_data = {}
def pytest_configure(config):
# 设置临时目录为 Unix 风格路径
temp_dir = '/tmp/pytest_temp'
os.environ['PYTEST_ADDOPTS'] = f'--basetemp={temp_dir}'
@pytest.fixture(autouse=True, scope="session")
def fix_session():
"""
会话级fixture
autouse:如果为 True,则对于可以看到它的所有测试,夹具函数将被激活。
如果为 False(默认值),需要显式引用才能激活夹具。
:return:
"""
print("fixture会话级:fix_session")
@pytest.fixture(autouse=True, scope="module")
def fix_module():
"""
模块级fixture
:return:
"""
print("fixture模块级:fix_module")
@pytest.fixture(autouse=True, scope="class")
def fix_class():
"""
类级fixture
:return:
"""
print("fixture类级:fix_class")
@pytest.fixture(autouse=True, scope="function")
def fix_func():
"""
方法级fixture
:return:
"""
print("fixture方法级:fix_func")
@pytest.fixture
def set_global_data():
"""
设置全局变量,用于关联参数
:return:
"""
def _set_global_data(key, value):
global_data[key] = value
return _set_global_data
@pytest.fixture
def get_global_data():
"""
从全局变量global_data中取值
:return:
"""
def _get_global_data(key):
return global_data.get(key)
return _get_global_data
def pytest_html_report_title(report):
"""报告标题"""
report.title = "自动单元测试报告"
# 运行测试前修改环境信息
def pytest_configure(config):
config.stash[metadata_key]["项目名称"] = "Gaia-V5 单元测试"
config.stash[metadata_key]["测试模块"] = "XXX场景-用户-创建"
config.stash[metadata_key]["测试环境"] = "集群环境:192.168.120.20"
config.stash[metadata_key]["测试版本"] = "V7.1.10.3"
config.stash[metadata_key]["测试日期"] = f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
def pytest_html_results_summary(prefix, summary, postfix):
"""报告摘要,测试人员信息"""
prefix.extend([html.p("所属部门: xxx研发部-研发五处")])
prefix.extend([html.p("测试人员: 张三、李四")])
def pytest_html_results_table_header(cells):
"""处理结果表的表头"""
# 往表格中增加一列Time,并且给Time增加排序
cells.insert(1, html.th("执行时间", class_="sortable time", col="time"))
# 往表格增加一列Description,并且给Description增加排序
cells.insert(2, html.th("用例描述", class_="sortable description", col="description"))
# 移除表格最后一列:Links
cells.pop(-1)
def pytest_html_results_table_row(report, cells):
"""处理结果表的每一行"""
# 往列Time插入每行的值
cells.insert(1, html.td(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), class_="col-time"))
# 往列Description插入每行的值
cells.insert(2, html.th(report.description, class_="sortable description", col="description"))
# 移除表格最后一列:Links
cells.pop(-1)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
def pytest_html_results_table_html(report, data):
"""清除执行成功的用例logs"""
if report.passed:
del data[:]
data.append("这条用例测试通过!")
@pytest.hookimpl(optionalhook=True)
def pytest_metadata(metadata):
"""取到标题栏"""
metadata.pop("Platform", None)
metadata.pop("Packages", None)
metadata.pop("JAVA_HOME", None)
metadata.pop("Plugins", None)
8. 用例编写步骤
python
复制代码
testPytest
-- tests
-- common # 公共方法模块
-- data # 测试用例数据模块
-- env-pytest3.6 # 单元测试运行环境.whl列表,通过 pip3 install env-pytest3.6 进行安装
-- example # 示例模块
-- logs # 单元测试日志模块,记录单元测试用例运行详细日志
-- sysmgt # 业务代码对应的单元测试用例模块
-- conftest.py # 定义hook函数集合
-- README.md # 单元测试模块说明书
-- requirements.txt # 单元测试运行环境,通过 pip3 install -r requirements.txt 进行安装
-- .coveragerc
-- pytest.ini # 单元测试用例运行时配置文件
-- run_unittest.py # 单元测试用例运行总入口
8.1 按照以下方式检查用例
python
复制代码
# 1.导入业务函数是否正确
from business.user_info import create_user
# 2.创建模拟数据,每个用例运行时都会有各自的测试数据,以类为例
class TestCreateUser(object):
"""
xxx
"""
@classmethod
def setup_class(cls):
# 模拟数据
pass
def setup_method(self):
# 模拟数据
pass
# 3.编写单元测试用例,根据业务函数分支、创建单元测试用例,使单元测试用例覆盖业务函数的所有分支
def test_create_user_id_success(self):
"""
:return:
创建用户成功
:rtype:
"""
# 1.模拟数据
logger.info(f"模拟数据:{self.request_info}")
# 2.调用方法
result = create_user(self.request_info)
logger.info(f"测试方法结果:{result}")
# 3.断言
assert json.loads(result) == RESULT_JSON
# 4.断言,检查断言是否正确
assert xxx
9. 单元测试示例
python
复制代码
# -*- coding: utf-8 -*-
"""
Author: xxx
date: 2025/3/3 10:53
Description:
"""
# 导入python包
import pytest
import random
# 从单元测试公共模块导入包
from tests.common.logging_utils import CustomLogger
# 导入待测试包
from business.funx import add
# 执行成功返回json格式
RESULT_JSON = {"version": 0, "operation_type": 'op', "error_code": 0}
# 定义日志模块
logger = CustomLogger("business").logger
def setup_module(module):
"""
测试模块级别的fixture,模块开始时执行一次
:param module:
:type module:
:return:
:rtype:
"""
logger.info("<<<========测试模块执行前调用:setup_module========>>>")
def teardown_module(module):
"""
测试模块级别的fixture,模块结束时执行一次
:param module:
:type module:
:return:
:rtype:
"""
logger.info("<<<========测试模块执行后调用:teardown_module========>>>")
def setup_function(function):
"""
测试函数级别的fixture,函数开始时执行一次
:param function:
:type function:
:return:
:rtype:
"""
logger.info("<<<========测试函数执行前调用:setup_function========>>>")
def teardown_function(function):
"""
测试函数级别的fixture,函数结束后时执行一次
:param function:
:type function:
:return:
:rtype:
"""
logger.info("<<<========测试函数执行后调用:teardown_function========>>>")
class TestFunx(object):
"""
业务代码,测试类
"""
@classmethod
def setup_class(cls):
"""
测试类级别的fixture,需要用@classmethod装饰器修饰
:return:
:rtype:
"""
cls.a = random.randint(1, 100)
cls.b = random.randint(200, 500)
logger.info("<<<========测试类执行前调用:setup_class========>>>")
@classmethod
def teardown_class(cls):
"""
测试类级别的fixture,需要用@classmethod装饰器修饰
:return:
:rtype:
"""
logger.info("<<<========测试类执行后调用:teardown_class========>>>")
def setup_method(self):
"""
测试方法级别的fixture
:return:
:rtype:
"""
logger.info("<<<========测试方法执行前调用:setup_method========>>>")
def teardown_method(self):
"""
测试方法级别的fixture
:return:
:rtype:
"""
logger.info("<<<========测试方法执行后调用:teardown_method========>>>")
def test_add_success(self):
"""
求两个数的和
:return:
:rtype:
"""
# 1.模拟数据
logger.info(f"模拟数据:{self.a}, {self.b}")
# 2.调用方法
result = add(self.a, self.b)
# 3.断言
assert result == self.a + self.b
def test_add_zero(self):
"""
求两个数的和
:return:
:rtype:
"""
# 1.模拟数据
self.a = 0
self.b = 0
logger.info(f"模拟数据:{self.a}, {self.b}")
# 2.调用方法
result = add(self.a, self.b)
# 3.断言
assert result == self.a + self.b
def test_add_negative(self):
"""
求两个数的和,负数
:return:
:rtype:
"""
# 1.模拟数据
self.a = -1
self.b = -2
logger.info(f"模拟数据:{self.a}, {self.b}")
# 2.调用方法
result = add(self.a, self.b)
# 3.断言
assert result == self.a + self.b
if __name__ == '__main__':
pytest.main()
10. 运行
python
复制代码
# 运行
python3 run_unittest.py
11. 覆盖率
python
复制代码
# 直接打开
unittest_coverage_html/index.html
python
复制代码
# 1. coverage
覆盖率:指的是代码被测试的程度。覆盖率越高,意味着代码被测试的部分越多,代码的质量通常也越高。
# 2. statements
语句:指的是代码中的可执行语句。覆盖率报告会统计这些语句中有多少被执行了。
# 3. missing
缺失:指的是代码中没有被测试到的语句。这些语句在测试过程中没有被执行到。
# 4. excluded
排除:指的是在覆盖率报告中被排除的语句。这些语句通常不会被测试,例如文档字符串、某些装饰器等。可以通过配置文件(如 .coveragerc)来指定哪些语句需要排除。
# 5. branches
分支:指的是代码中的条件分支,例如 if 语句、for 循环等。覆盖率报告不仅会统计语句的覆盖率,还会统计分支的覆盖率,确保条件分支都被测试到。
# 6. partial
部分:指的是部分分支被测试到的情况。例如,一个 if-else 语句中只有 if 分支被执行了,而 else 分支没有被执行,这种情况就被认为是部分覆盖率。
12. 单个文件覆盖率详情
python
复制代码
# 绿色:表示代码已经被测试覆盖。
# 红色:表示代码没有被测试覆盖。
# 灰色:表示这些行被排除在覆盖率统计之外。
# 黄色:表示部分分支被测试到,但不是所有分支都被测试到。
14. 测试报告
python
复制代码
# 直接打开
unittest_report.html
15.Mock
参考官网进行学习并使用:26.6. unittest.mock 上手指南 --- Python 3.6.15 文档
最新文档:unittest.mock --- 新手入门 --- Python 3.13.2 文档
15.1 什么是Mock
python
复制代码
# Mock就是要模拟函数、方法、类的行为
# 使用场景:
# 1.模拟函数调用
# 2.记录在对象上的方法调用
模块 |
功能 |
mock |
mock.Mock |
mock |
mock.patch |
mock |
mock.patch.object |
mock |
mock.multiple |
mock |
mock.dict |
python
复制代码
# Mock对象是模拟的基石,提供了丰富多彩的功能。从测试的阶段来分类包括:
模块 |
功能 |
函数 |
描述 |
Mock |
构造器 |
name |
mock对象的名称,是mock对象的标识 |
Mock |
构造器 |
return_value |
mock对象被调用时的返回值 |
Mock |
构造器 |
side_effect |
mock对象被调用时的返回,可以是函数,类等,覆盖return_value |
Mock |
构造器 |
spec |
将对象设置成mock的属性 |
Mock |
构造器 |
spce_set |
严格限制mock对象的属性访问,访问不存在的属性报错 |
Mock |
构造器 |
wraps |
模拟对象是装饰器的使用 |
Mock |
构造器 |
unsafe |
默认情况下,如果任何以assert或assret开头的属性都将引发错误,当unsafe=True时可以访问 |
Mock |
断言方法 |
assert_not_called |
模拟从未被调用过 |
Mock |
断言方法 |
assert_called |
至少调用了一次模拟 |
Mock |
断言方法 |
assert_called_once |
仅调用了一次模拟 |
Mock |
断言方法 |
assert_called_with |
使用指定的参数调用模拟 |
Mock |
断言方法 |
assert_called_once_with |
模拟被调用了一次,并且该调用使用了指定的参数 |
Mock |
断言方法 |
assert_any_call |
已使用指定的参数调用了模拟 |
Mock |
管理方法 |
attach_mock |
将一个mock对象添加到另一个mock对象中 |
Mock |
管理方法 |
configure_mock |
更改mock对象的return_value值 |
Mock |
管理方法 |
mock_add_spec |
给mock对象添加新的属性 |
Mock |
管理方法 |
reset_mock |
将mock对象回复到测试之前的状态 |
Mock |
统计方法 |
called |
标识是否调用过 |
Mock |
统计方法 |
call_count |
返回调用的次数 |
Mock |
统计方法 |
call_args |
获取调用时的参数 |
Mock |
统计方法 |
call_args_list |
获取调用的所有参数,结果是一个列表 |
Mock |
统计方法 |
method_calls |
测试一个mock对象都调用了哪些方法,返回一个列表 |
Mock |
统计方法 |
mock_calls |
测试对象对mock对象所有的调用,结果是一个列表 |
15.2 Mock的定义
python
复制代码
# Mock的定义
class unittest.mock.Mock(spec=None, side_effect=None, return_value=DEFAULT, wraps=None, name=None, spec_set=None, unsafe=False, **kwargs)
spec: 参数可以把一个对象设置为 Mock 对象的属性。访问mock对象上不存在的属性或方法时,将会抛出属性错误。
side_effect :调用mock时的返回值,可以是函数,异常类,可迭代对象。使用side_effect可以将模拟对象的返回值变成函数,异常类,可迭代对象等。
当设置了该方法时,如果该方法返回值是DEFAULT,那么返回return_value的值,如果不是,则返回该方法的值。 return_value 和 side_effect 同时存在,side_effect会返回。
如果 side_effect 是异常类或实例时,调用模拟程序时将引发异常。
如果 side_effect 是可迭代对象,则每次调用 mock 都将返回可迭代对象的下一个值。
return_value :调用mock的返回值,模拟某一个方法的返回值。
wraps: 装饰器:模拟对象要装饰的项目。
如果wrapps不是None,那么调用Mock将把调用传递给wrapped对象(返回实际结果)。
对mock的属性访问将返回一个mock对象,该对象装饰了包装对象的相应属性。
name :mock 的名称。 这个是用来命名一个mock对象,只是起到标识作用,当你print一个mock对象的时候,可以看到它的name。
spec_set:更加严格的要求,spec_set=True时,如果访问mock不存在属性或方法会报错
15.3 Mock的使用
python
复制代码
使用mock.Mock()可以创建一个mock对象,对象中的方法有两种设置途径:
作为Mock类的参数传入。
mock.Mock(return_value=20,side_effect=mock_fun, name='mock_sum')
实例化mock对象之后设置属性。
mock_sum = mock.Mock()
mock_sum.return_value = 20
mock_sum.side_effect = mock_fun
15.4 Mock与MagicMock
python
复制代码
# unittest.mock模块提供了Mock和MagicMock:
Mock主要模拟指定的方法和属性
MagicMock是Mock的子类,同时模拟了很多Magic方法(len, __str__等等)
如果执行代码逻辑中,某一个函数不想被真的调用的话,可以使用Mock方法
15.5 Mock示例
python
复制代码
from unittest.mock import Mock
# 创建一个Mock对象
mock = Mock()
# 模拟find_person 函数,在执行测试时执行到find_person这个函数,会直接返回{"id" :1, "name":"jack"},而不是真正的去执行函数
mock.find_person.return_value = {
"id" :1,
"name":"jack"
}
# 调用find person函数
print(mock.find_person())
16. MagicMock
python3
复制代码
# MagicMock是Mock的一个子类,具有大多数魔法方法的默认实现。在mock.patch中new参数如果没写,默认创建的是MagicMock。
魔法方法:Python 中的类有一些特殊的方法。在python的类中,以两个下画线__开头和结尾的方法如__new__,__init__ 。
这些方法统称"魔术方法"(Magic Method)。任意自定义类都会拥有魔法方法。
使用魔术方法可以实现运算符重载,如对象之间使用 == 做比较时,其实是对象中 __eq__实现的。
魔法方法类似于对象默认提供的各种方法。
__new__ 创建类并返回这个类的实例
__init__ 可理解为"构造函数",在对象初始化的时候调用,使用传入的参数初始化该实例
__del__ 可理解为"析构函数",当一个对象进行垃圾回收时调用
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__ge__
__getattribute__
__gt__
__hash__
__init_subclass__
__le__
__lt__
__module__
__ne__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
__weakref__
Magic Mock 的默认值:
Magic Mock 实例化之后就会有一些初始值,是一些属性的实现。具体的默认值如下:
__lt__: NotImplemented
__gt__: NotImplemented
__le__: NotImplemented
__ge__: NotImplemented
__int__: 1
__contains__: False
__len__: 0
__iter__: iter([])
__exit__: False
__aexit__: False
__complex__: 1j
__float__: 1.0
__bool__: True
__index__: 1
__hash__: default hash for the mock
__str__: default str for the mock
__sizeof__: default sizeof for the mock
使用MagicMock和Mock的场景:
使用MagicMock:需要魔法方法的场景,如迭代
使用Mock:不需要魔法方法的场景可以用Mock
17.Patch
17.1 patch能做什么
python
复制代码
# patch可以临时用Mock对象替换一个目标(函数,方法,类)
Patch()充当函数修饰器、类修饰器或上下文管理器。在函数体或with语句中,使用patch中的new替换目标函数或方法。当function/with语句退出时,模拟程序被撤消。
功能 |
函数 |
描述 |
mock.patch |
target |
模拟的目标对象 |
mock.patch |
new |
默认对target的替换,模拟返回的结果 |
mock.patch |
new_callable |
模拟返回的结果哦,是可调用对象,会覆盖new |
mock.patch |
spec |
为mock对象添加属性 |
mock.patch |
spec_set |
属性限制,访问mock没有的属性会报错 |
mock.patch |
autospec |
标记mock对象属性全部被spce替换 |
mock.patch |
create |
允许访问mock对象不存在的属性 |
python
复制代码
# target:模拟对象的路径,参数必须是一个str,格式为 'package.module.ClassName',注意这里的格式一定要写对。如果对象和mock函数在同一个文件中,路径要加文件名
# new:模拟返回的结果,是一个具体的值,也可是函数。new属性替换target,返回模拟的结果。
# new_callable:模拟返回的结果,是一个可调用的对象,可以是函数。
# spec:与Mock对象的参数类似,用于设置mock对象属性。
# spec_set:与Mock对象的参数类似,严格限制mock对象的属性或方法的访问。
# autospec:替换mock对象中所有的属性。可以替换对象所有属性,但不包括动态创建的属性。
autospec是一个更严格的规范。如果你设置了autospec=True,将会使用spec替换对象的属性来创建一个mock对象。mock对象的所有属性都会被spec相应的属性替换。
被mock的方法和函数会检查他们的属性,如果调用它们没有属性会抛出 TypeError。它们返回的实例也会是相同属性的类。
# create:允许访问不存在属性
默认情况下,patch()将无法替换不存在的属性。如果你通过create=True,当替换的属性不存在时,patch会创建一个属性,并且当函数退出时会删除掉属性。这对于针对生产代码在运行时创建的属性编写测试非常有用。它在默认情况下是关闭的,因为它可能是危险的,打开它后,您可以针对实际不存在的api编写通过测试的代码
同时mock.patch也是mock的一个子类,所以可以用return_value 和 side_effect 直接使用 return_value
17.2 patch的目标
python
复制代码
# patch可以替换的目标:
1.目标必须是可以 import 的
2.是在使用目标的地方替换而不是替换定义
17.3 patch定义
python
复制代码
unittest.mock.patch(target, new=DEFAULT, spec=None, create=False, spec_set=None, autospec=None, new_callable=None, **kwargs)
17.3 patch的使用方式
python
复制代码
# 1.手动方式
# 2.装饰器方式
# 3.上下文管理器方式
- |
手动指定 |
装饰器 |
上下文管理器 |
优点 |
可以更精细控制mock的范文 |
方便mock多个对象 |
|
不足之处 |
需要手动start和stop |
装饰器顺序和珊瑚参数相反容易混乱 |
一个with只能mock一个对象 |
python
复制代码
# patch的最佳实践:
patch有三种使用方法,最佳的使用实践是装饰器形态。
手动指定方法需要start和stop控制,过于繁琐,虽然存在一个stopall的方法,但是仍然要逐个start
with写法的缺点很明显,一次不可以mock多个目标,多个with层层缩进很明显不可能。
最佳实践:装饰器方法可以方便的mock多个对象,只需要熟悉装饰的顺序和函数参数的对应关系。
patch中可以return_value和new都可以改变结果,推荐patch中使用new属性,Mock中使用return_value.
17.3.1 装饰器方式
python
复制代码
import unittest
from unittest.mock import Mock,patch
from myprj.service import student_service
class TestStudentService(unittest.TestCase):
@patch("myprj.service.student_service.save_student")
@patch("myprj.service.student_service.find_student_by_id") # 把find_student_by_id函数替换成Mock对象
def test_change_name_detector(self, find_student_mock, save_student_mock):
'''参数find_student_mock与save_student_mock上述@patch是逆序的方式
'''
# setup
student = Mock(id=1, name = 'Jack')
find_student_mock.return_value = student
# Action
student_service.change_name(1, 'Tom')
# Assert
self.assertEqual('Tom', student.name)
17.3.2 上下文管理器
python
复制代码
def test_change_name_contextmanager(self):
# setup
student = Mock(id=1, name = 'Jack')
find_student_mock.return_value = student
with patch("myprj.service.student_service.find_student_by_id") as find_student_mock, \
patch("myprj.service.student_service.save_student"):
# Action
find_student_mock.return_value = student
student_service.change_name(1, 'Tom')
# Assert
self.assertEqual('Tom', student.name)
17.3.3 手动方式
python
复制代码
@patch("myprj.service.student_service.find_student_by_id")
def test_change_name_manual(self,find_student_mock):
# setup
student = Mock(id=1, name = 'Jack')
find_student_mock.return_value = student
patcher = patch("myprj.service.student_service.save_student")
patcher.start()
# Action
student_service.change_name(1, 'Tom')
patcher.stop()
# Assert
self.assertEqual('Tom', student.name)