当前位置: 首页 > article >正文

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

一、用法

.1 断言

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

# 一个成功的测试函数

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 通常用于调试和测试过程中验证程序的某些假设或条件是否成立。

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

 .2 测试预期异常

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

import pytest

def test_multiple_exceptions():
    
    # 用 Pytest 提供的一个上下文管理器,用于测试代码是否会抛出特定的异常。
    with pytest.raises((TypeError, ValueError)):  
        int("abc") #ValueError
        len(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 raised
    except 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 raised
    except Exception as e:
        assert False, f"Unexpected exception raised: {type(e).__name__}"

例子2:

import pytest


def 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 可以返回任何类型的数据,包括对象、列表、字典等。

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 的作用域:

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

 3.3 Fixture 的参数化 (params)

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

import pytest

@pytest.fixture(params=[1, 2, 3])
def test_data(request):
    return request.param

def 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: 一个列表,包含参数的值。
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 会对参数进行组合,生成更多的测试用例。

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,以便更清晰地识别测试结果。

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 实例。

import pytest

@pytest.fixture(params=["file1.txt", "file2.txt"])
def test_file(request):
    filename = request.param
    with open(filename, "w") as f:
        f.write("test data")
    yield filename
    import os
    os.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 是标记的名称。

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 参数用于提供跳过原因。

    @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,则跳过测试。

    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 参数用于指定预期的异常类型。

    @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 run
    ui: mark test as ui test
    regression: 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 和参数化结合使用,以实现更复杂的测试场景。例如,你可以使用标记来选择运行哪些参数化测试:

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.kler.cn/a/467073.html

相关文章:

  • Flask是什么?深入解析 Flask 的设计与应用实践
  • 【AutoSAR】【底软自动化】Arxml自动配置平台
  • 现代光学基础5
  • UniApp 状态管理:Vuex 在 UniApp 中的实践
  • 如何判断状态:停留还是移动。【计算加速度de方案】
  • 【重庆】《政务数字化应用费用测算规范》(T/CDCIDA 001—2023)-省市费用标准解读系列36
  • 面向对象分析和设计OOA/D,UML,GRASP
  • vue代理问题
  • 对计网大题的一些指正(中间介绍一下CDM的原理和应用)
  • 51单片机——按键实验
  • YOLOv10-1.1部分代码阅读笔记-autobackend.py
  • python3GUI--智慧交通监控与管理系统 By:PyQt5
  • Chromebook 的 4 个最佳变声器
  • Dart语言的软件工程
  • 回调机制实现观察者模式
  • 什么是索引
  • PyTorch FlexAttention技术实践:基于BlockMask实现因果注意力与变长序列处理
  • SMMU软件指南之系统架构考虑
  • 【玩转全栈】----Django连接MySQL
  • Verilog语法之generate与genvar用法
  • maven 打包时优先选择本地仓库
  • 小程序学习06——uniapp组件常规引入和easycom引入语法
  • VSCode设置ctrl或alt+mouse(left)跳转
  • 计算机毕业设计Python+Spark中药推荐系统 中药识别系统 中药数据分析 中药大数据 中药可视化 中药爬虫 中药大数据 大数据毕业设计 大
  • 网络攻击原理与常用方法
  • 启航数据结构算法之雅舟,悠游C++智慧之旅——线性艺术:顺序表之细腻探索