单元测试-pytest框架实践

文章目录

  • [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 # 测试用例模块目录
        • test_xxx.py # 测试文件
      • 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

7. conftest.py

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)
相关推荐
qq_白羊座2 小时前
pytest框架 核心知识的系统复习
pytest
小马爱打代码18 小时前
Spring Boot 测试:单元、集成与契约测试全解析
spring boot·后端·单元测试
码叔义1 天前
slf4j和log4j的区别与使用
python·单元测试·log4j
测试老哥1 天前
如何提高测试用例覆盖率?
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·测试覆盖率
evelol71 天前
【pytest框架源码分析四】pluggy源码分析之hook执行
自动化·pytest
fzm52982 天前
嵌入式软件测试工具的“安全与效率悖论”破局之道
自动化测试·单元测试·汽车·嵌入式·白盒测试
图图图图爱睡觉2 天前
用大白话解释日志处理Log4j 是什么 有什么用 怎么用
单元测试·log4j
前端安迪2 天前
Playwright中修改接口返回的5种方法
前端·单元测试