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

embedded/2025/1/8 5:20:18/

一、用法

.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/embedded/151385.html

相关文章

292-基于3U VPX 4核8线程I7 X86主板

一、产品概述 该产品是一款基于第六代Intel i7四核八线程处理器的高性能3U VPX刀片式计算机。产品提供了4个x4 PCIe 3.0总线接口&#xff0c;其中2个x4 PCIe 3.0接口可配置为1个x8 PCIe3.0接口&#xff0c;另外2个x4 PCIe 3.0接口可灵活配置成4个x2 PCIe 3.0或8个x1 PCIe 3.…

无线网络施工布线注意事项

路由器到交换机&#xff0c;交换机到交换机之间&#xff1a;普通450一箱的超五类线&#xff08;在用&#xff09;&#xff0c;建议长度70米以内&#xff08;特殊环境下可最高90米以内&#xff0c;需提前测试&#xff0c;未布线前现场接90米线测试。&#xff09;、六类线&#x…

【单片机】NPN+PNP组成的高边开关无法完全关断

项目场景&#xff1a; 采用NPNPNP组成高边开关&#xff0c;由单片机GPIO控制。 问题描述 原理图如下。发现CPS_ENABLE为低电平时&#xff0c;3.3V_C的电平不为0&#xff0c;约为0.9V。 原因分析与解决方案&#xff1a; 从原理上分析和独立电路测试没有问题&#xff0c;那么…

Unresolved plugin: ‘org.apache.maven.plugins:maven-site-plugin:3.12.1‘

问题 使用idea 社区办加载项目提示下面问题&#xff1a; Unresolved plugin: org.apache.maven.plugins:maven-site-plugin:3.12.1 问题解决 maven插件地址&#xff1a; https://maven.apache.org/plugins/maven-dependency-plugin/plugins.html Maven 中央仓库地址&#…

Effective C++读书笔记——item8(析构函数与异常)

析构函数引发异常的问题 异常同时存在的隐患&#xff1a;C 虽未禁止在析构函数中引发异常&#xff0c;但坚决阻止这样做。以std::vector等容器包含对象为例&#xff0c;当容器析构时要析构其中元素&#xff0c;若在析构元素&#xff08;如Widget类对象&#xff09;过程中连续抛…

客户案例:基于慧集通平台集成打通小满CRM+金蝶云星空+钉钉

一、引言 本案例原型公司是一家生物科技公司&#xff0c;公司自开创以来专注于体外诊断生物活性原材料的研究、生产、销售和服务&#xff0c;致力于为全球体外诊断试剂生产企业提供领先且具有竞争力的核心原料和相关辅助产品服务。公司以卓越的产品和优质的服务赢得了客户的广…

项目优化之策略模式

目录 策略模式基本概念 策略模式的应用场景 实际项目中具体应用 项目背景&#xff1a; 策略模式解决方案&#xff1a; 计费模块策略模式简要代码 策略模式基本概念 策略模式(Strategy Pattern) 是一种行为型设计模式&#xff0c;把算法的使用放到环境类中&#xff0c;而算…

初步认识UML

在软件开发中&#xff0c;理解系统的结构、设计和流程对团队协作至关重要。统一建模语言&#xff08;Unified Modeling Language&#xff0c;简称 UML&#xff09;是一种标准化的图形化语言&#xff0c;帮助开发者通过图表直观地描述和设计系统。无论是软件架构师、开发者&…