深入浅出 Pytest:自动化测试的最佳实践 pytest教程 程序测试 单元化测试

news/2025/1/8 4:17:44/

一、用法

.1 断言

         在测试函数中用assert(断言)来判断测试是否符合预期。

python"># 一个成功的测试函数def test_passing():assert (1, 2, 3) == (1, 2, 3)# 一个失败的测试函数def test_failing():assert (1, 2, 3) == (3, 2, 1)

         assert 语句本质上是用于检查一个表达式的布尔值是否为 True。如果表达式的值为 False,则会抛出一个 AssertionError 异常。assert 通常用于调试和测试过程中验证程序的某些假设或条件是否成立。

python"># Syntax
assert 条件表达式 [, 错误信息]
  • 条件表达式:任何返回布尔值的表达式。
  • 错误信息(可选):如果断言失败,会作为异常的附加信息显示。

 .2 测试预期异常

        pytest 捕获异常的主要目的是为了验证代码在预期抛出异常的情况下是否能够正确处理这些异常。简单来说,就是测试你的代码在遇到错误情况时是否“按计划出错”,而不是直接崩溃或产生不可预料的行为。
例子1:(用pytest.raises上下管理器可以让测试代码更清晰、易读,并提高异常相关测试的可靠性)

python">import pytestdef test_multiple_exceptions():# 用 Pytest 提供的一个上下文管理器,用于测试代码是否会抛出特定的异常。with pytest.raises((TypeError, ValueError)):  int("abc") #ValueErrorlen(1) # TypeError# 以上代码等效于:
def test_multiple_exceptions():try:int("abc")assert False, "Expected ValueError but no exception was raised"except ValueError:pass  # Test passes because ValueError was raisedexcept Exception as e:assert False, f"Unexpected exception raised: {type(e).__name__}"try:len(1)assert False, "Expected ValueError but no exception was raised"except TypeError:pass  # Test passes because TypeError was raisedexcept Exception as e:assert False, f"Unexpected exception raised: {type(e).__name__}"

例子2:

python">import pytestdef f():raise ExceptionGroup("Group message",[RuntimeError(),],)def test_exception_in_group():with pytest.raises(ExceptionGroup) as excinfo:f()assert excinfo.group_contains(RuntimeError)assert not excinfo.group_contains(TypeError)

        这段代码使用 pytest 测试了一个函数是否正确地创建并抛出了包含特定类型异常的异常组,并验证了该异常组中是否不包含指定(TypeError)类型的异常。

二、Pytest 测试发现的约定

        在使用 pytest 进行自动化测试时,pytest 会根据一定规则自动寻找项目中的测试用例。以下是详细的解释: 

1.1 测试发现的起始位置

  • 如果没有指定测试路径(testpaths),pytest 会默认从当前目录开始查找测试文件。
  • 如果在配置中设置了 testpaths(例如在 pytest.ini 配置文件中),pytest 会从指定的路径开始查找。

1 .2 查找测试文件

  • pytest 会寻找以 test_ 为前缀,或者以 _test 为后缀的 Python 文件。这意味着像 test_example.pyexample_test.py 这样的文件名会被识别为测试文件。
1.3 查找测试函数和方法

在这些测试文件中,pytest 会查找符合特定规则的测试函数和测试方法:

  • 类外的测试函数或方法:test 开头的函数名(如 test_addition())会被认为是测试函数。这些函数不需要属于任何类,区别于unitest,这使pytest的语法更加灵活。

  • 类内的测试方法: 如果函数位于类中,并且该类的名称以 Test 开头(例如 TestMathOperations),且该类没有 __init__ 方法(即没有初始化方法),则该类中的所有以 test 开头的方法也会被视为测试方法。

  • 静态方法和类方法: 即使是静态方法(@staticmethod)和类方法(@classmethod),如果它们以 test 开头,也会被认为是测试方法。

         可以在程序中执行pytest.main()函数来运行该程序内所有的测试函数和测试方法,或者在终端执行pytest <文件名>。

三、Fixture(固件)

3.1 Fixture 的定义

        使用 @pytest.fixture 装饰器来定义 fixture。Fixture 可以返回任何类型的数据,包括对象、列表、字典等。

python">import pytest@pytest.fixture
def database_connection():# 建立数据库连接conn = ... # 模拟数据库连接print("setup")yield conn  # 将连接提供给测试函数print("teardown")# 关闭数据库连接conn.close()def test_using_database(database_connection):# 使用 fixture 提供的数据库连接print("test run")assert ... # 使用conn进行断言
  • @pytest.fixture 装饰器定义了一个名为 database_connection 的 fixture。
  • yield conn 语句将模拟的数据库连接 conn 提供给测试函数。yield 之前的代码会在测试函数运行前执行(setup预处理),yield 之后的代码会在测试函数运行后执行(teardown后处理)。
  • test_using_database 函数通过参数 database_connection 使用了这个 fixture。pytest 还会自动将 fixture 的返回值传递给该参数(此例未体现)。
  • 这个例子展示了 fixture 如何进行 setup 和 teardown 操作,模拟建立和关闭数据库连接,或者创建和清理临时文件等。

3.2 Fixture 的作用域 (Scope)

Fixture 的作用域决定了 fixture 的生命周期。pytest 提供了以下几种作用域:

  • function (默认): 每个测试函数都会调用一次 fixture。
  • class: 每个测试类中的所有测试方法共享一个 fixture 实例。
  • module: 每个模块中的所有测试函数共享一个 fixture 实例。
  • session: 整个测试会话只创建一个 fixture 实例。

可以通过 scope 参数来指定 fixture 的作用域:

python">@pytest.fixture(scope="module")
def module_resource():# ...yield

 3.3 Fixture 的参数化 (params)

        可以使用 params 参数为 fixture 提供多组参数,从而实现参数化测试。这会在测试执行时,为每组参数运行一次测试函数。

python">import pytest@pytest.fixture(params=[1, 2, 3])
def test_data(request):return request.paramdef test_with_parameterized_fixture(test_data):print(f"Testing with data: {test_data}")assert test_data > 0
  • @pytest.fixture(params=[1, 2, 3]) 使用 params 参数定义了一个参数化的 fixture。
  • request.param 获取当前参数的值。
  • test_with_parameterized_fixture 函数使用 test_data fixture。pytest 会依次使用 123 作为 test_data 的值,分别运行测试函数三次。
  • 这个例子展示了如何使用 fixture 进行参数化测试,简化了重复的测试代码。

 四、参数化 (Parametrization)

        参数化允许你使用不同的输入数据多次运行同一个测试函数。这可以有效地减少测试代码的重复,并提高测试的覆盖率。

4.1 使用 @pytest.mark.parametrize 装饰器

使用 @pytest.mark.parametrize 装饰器来定义参数化测试。该装饰器接受两个参数:

  • argnames: 一个字符串或字符串列表,表示参数的名称。
  • argvalues: 一个列表,包含参数的值。
python">import pytest@pytest.mark.parametrize("input, expected", [(2, 4),(3, 9),(4, 16),
])
def test_square(input, expected):assert input * input == expected
  • @pytest.mark.parametrize("input, expected", ...) 装饰器定义了参数化测试。
  • "input, expected" 指定了两个参数的名称。
  • [(2, 4), (3, 9), (4, 16)] 提供了三组参数值。
  • pytest 会依次使用这三组参数值运行 test_square 函数三次。

4.2 参数的组合

        如果使用多个 @pytest.mark.parametrize 装饰器,pytest 会对参数进行组合,生成更多的测试用例。

python">import pytest@pytest.mark.parametrize("x", [1, 2])
@pytest.mark.parametrize("y", [3, 4])
def test_addition(x, y):assert x + y > 0

这个例子会生成 4 个测试用例:(1, 3), (1, 4), (2, 3), (2, 4)

4.3 参数 ID

可以使用 ids 参数为每个测试用例指定一个 ID,以便更清晰地识别测试结果。

python">import pytest@pytest.mark.parametrize("input, expected", [(2, 4),(3, 9),(4, 16),
], ids=["two", "three", "four"])
def test_square(input, expected):assert input * input == expected
  • ids=["two", "three", "four"] 为每个测试用例指定了 ID。
  • 在测试结果中,会显示这些 ID,而不是默认的参数值,使测试结果更易读。

4.4 与 Fixture 结合使用

参数化可以与 fixture 结合使用,为每个参数化的测试用例提供不同的 fixture 实例。

python">import pytest@pytest.fixture(params=["file1.txt", "file2.txt"])
def test_file(request):filename = request.paramwith open(filename, "w") as f:f.write("test data")yield filenameimport osos.remove(filename)@pytest.mark.parametrize("content", ["hello", "world"])
def test_file_content(test_file, content):with open(test_file, "r") as f:f.write(content) #根据参数写入内容with open(test_file, "r") as f:file_content = f.read()assert file_content == content

         这个例子结合了两者。test_file fixture 创建了两个不同的文件,而 @pytest.mark.parametrize 提供了不同的内容来写入这些文件。test_file_content 函数会运行四次,分别测试不同的文件和内容组合

五、标记函数

        pytest 的标记 (marks) 功能允许你为测试函数添加元数据,从而实现更灵活的测试组织、选择和执行。你可以使用标记来:

  • 对测试进行分类: 例如,将测试分为单元测试、集成测试、UI 测试等。
  • 跳过或预期失败的测试: 例如,跳过某些已知会失败的测试,或者标记某些测试为预期失败。
  • 控制测试执行: 例如,只运行特定标记的测试。

5.1 使用 @pytest.mark 装饰器

使用 @pytest.mark.markname 装饰器来为测试函数添加标记,其中 markname 是标记的名称。

python">import pytest@pytest.mark.slow  # 标记为慢速测试
def test_long_running_task():# ...@pytest.mark.ui  # 标记为 UI 测试
def test_user_interface():# ...@pytest.mark.parametrize("input, expected", [(1, 1), (2, 4)])
@pytest.mark.regression # 标记为回归测试
def test_square(input, expected):assert input * input == expected

一个测试函数可以拥有多个标记,就像 test_square 函数同时拥有 parametrizeregression 两个标记。

5.2 内置标记

pytest 提供了一些内置标记,用于常见的测试场景:

  • @pytest.mark.skip(reason="reason"):无条件跳过测试。reason 参数用于提供跳过原因。

    python">@pytest.mark.skip(reason="This test is currently under development")
    def test_feature_not_implemented_yet():# ...
    
  • @pytest.mark.skipif(condition, reason="reason"):根据条件跳过测试。condition 是一个 Python 表达式,如果值为 True,则跳过测试。

    python">import sys@pytest.mark.skipif(sys.platform == "win32", reason="This test only runs on Linux")
    def test_linux_specific_feature():# ...
    
  • @pytest.mark.xfail(reason="reason", raises=ExceptionType):标记测试为预期失败。即使测试失败,也不会将其计为失败,而是计为 xfail。raises 参数用于指定预期的异常类型。

    python">@pytest.mark.xfail(reason="This feature has a known bug")
    def test_buggy_feature():# ...@pytest.mark.xfail(raises=ZeroDivisionError)
    def test_division_by_zero():1 / 0
    
  • @pytest.mark.parametrize:正如之前提到的,用于参数化测试。

5.3 自定义标记

        你可以创建自定义标记,用于更灵活地组织和选择测试。你需要在 pytest.ini 文件中注册自定义标记(推荐这样子做),以避免警告。

pytest.ini 文件示例:

[pytest]
markers =slow: mark test as slow to runui: mark test as ui testregression: mark test as regression test

5.4 使用标记运行测试

使用 -m 选项可以运行带有特定标记的测试。

  • 运行所有标记为 slow 的测试:

    pytest -m slow
    
  • 运行所有标记为 slowui 的测试:

    pytest -m "slow or ui"
    
  • 运行所有未标记为 slow 的测试

    pytest -m "not slow"
    

5.5 结合 Fixture 和参数化

        标记可以与 Fixture 和参数化结合使用,以实现更复杂的测试场景。例如,你可以使用标记来选择运行哪些参数化测试:

python">import pytest@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"])
def input_value(request):return request.param@pytest.mark.fast
def test_fast_calculation(input_value):assert input_value * 2 < 10@pytest.mark.slow
def test_slow_calculation(input_value):assert input_value ** 2 < 20# 只运行标记为 fast 的测试
# pytest -m fast
# 只运行标记为 slow 的测试
# pytest -m slow
# 同时运行 fast 和 slow 的测试
# pytest -m "fast or slow"

http://www.ppmy.cn/news/1561458.html

相关文章

【保姆级爬虫】微博关键词搜索并获取博文和评论内容(python+selenium+chorme)

微博爬虫记录 写这个主要是为了防止自己忘记以及之后的组内工作交接&#xff0c;至于代码美不美观&#xff0c;写的好不好&#xff0c;统统不考虑&#xff0c;我只能说&#xff0c;能跑就不错了&#xff0c;上学压根没学过python好吧&#xff0c;基本上是crtlc&ctrlv丝滑小…

JavaScript网页设计案例:响应式动态购物车

在现代网页开发中&#xff0c;购物车是电子商务网站的重要功能之一。通过JavaScript&#xff0c;我们可以实现一个响应式动态购物车&#xff0c;提供用户友好的体验&#xff0c;并展示前端开发的核心能力。 案例需求 我们的购物车需要实现以下功能&#xff1a; 动态添加商品&…

【C语言程序设计——选择结构程序设计】求一元二次方程的根(头歌实践教学平台习题)【合集】

目录&#x1f60b; 任务描述 相关知识 sqrt() 函数 编程要求 测试说明 通关代码 测试结果 任务描述 本关任务&#xff1a;根据求根公式&#xff0c;计算并输出一元二次方程的两个实根&#xff0c;要求精确道小数点后2位。要求方程系数从键盘输入。如果输入的系数不满足求…

开源模型应用落地-qwen2-7b-instruct-LoRA微调合并-ms-swift-单机单卡-V100(十三)

一、前言 本篇文章将使用ms-swift去合并微调后的模型权重,通过阅读本文,您将能够更好地掌握这些关键技术,理解其中的关键技术要点,并应用于自己的项目中。 二、术语介绍 2.1. LoRA微调 LoRA (Low-Rank Adaptation) 用于微调大型语言模型 (LLM)。 是一种有效的自适应策略,…

【C++】P5732 【深基5.习7】杨辉三角

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目描述输入输出格式输入输出 输入输出样例 &#x1f4af;我的实现方法实现解析实现的优点改进空间 &#x1f4af;老师的实现方法实现解析优缺点对比 &#x1f4af;优化与…

【商城的功能开发】

商城的功能开发是一个复杂且多方面的过程&#xff0c;涉及前端和后端的开发、用户界面设计、数据库管理、支付系统集成等多个环节。以下是一些关键功能和步骤&#xff0c;可以帮助你了解商城开发的基本流程&#xff1a; 一、需求分析 目标用户&#xff1a;确定目标市场和用户需…

Redis 实现分布式锁

文章目录 引言一、Redis的两种原子操作1.1 Redis 的原子性1.2 单命令1.3 Lua 脚本1.4 对比单命令与 Lua 脚本 二、Redis 实现分布式锁2.1 分布式锁的概念与需求2.1.1 什么是分布式锁&#xff1f;2.1.2 分布式锁的常见应用场景 2.2 基于 Redis 的分布式锁实现2.2.1 锁的获取与释…

CPU过剩是什么意思? 有什么对电脑的影响吗?如何确认CPU有没有过剩

CPU 过剩通常是指计算机系统中 CPU 的性能远远超出了当前运行任务的需求。以下从产生原因和对电脑的影响为你详细介绍&#xff1a; 产生原因 硬件升级与软件发展不同步&#xff1a;用户为追求高性能提前升级了 CPU&#xff0c;而当前的软件应用程序在算法和功能上没有太大突破&…