pytest-testkit

pytest用例环境注册和日志记录插件

安装

仓库发行版中下载whl

wget https://gitcode.com/openlibing/openlibing-pytest-executor/releases/download/v1.0.0-alpha/pytest_testkit-1.0.0-py3-none-any.whl

安装

pip install --force-reinstall pytest_testkit-1.0.0-py3-none-any.whl

本地调试测试用例执行

安装完成后,pytest 会自动使用插件。

测试仓库下生成 testbed 模版:

pytest --collect-only --outtestbed=testbed.json

生成的 testbed.json 举例如下:

{
  "devices": [
    {
      "template_type": "multi_device_cover_single_type",
      "env_definition": [
        {
          "name": "device_1",
          "ip": "",
          "port": 22,
          "user": "",
          "pwd": ""
        },
        {
          "name": "device_2",
          "ip": "",
          "port": 22,
          "user": "",
          "pwd": ""
        }
      ]
    }
  ]
}

修改 testbed.json 中设备信息后,执行下面命令即可识别环境信息,执行测试用例:

pytest --testbed=testbed.json testcase/test_demo.py -k test_single_device_get_npu_info -s

在 pytest 中,你可以通过在命令行中使用 :: 符号来精确指定要执行的测试文件、测试类或单个测试方法,如: 文件名::类名::方法名 。

执行后,可以在 根目录/logs/TestCases 下查看用例的html格式日志

💡 提示:如果想要调试 环境调度直接分配的是容器场景的话,可以在环境上拉起容器时设置映射的端口,这样本地调试就可以直接连接上容器,模拟直接 在容器内执行命令的场景, 映射的命令:

docker run -d --name <name> -p 2222:22 <image>:<tag>

logger 使用

导入 logger:

from pytest_testkit.lib.common.log import logger

使用 logger 记录日志:

logger.info(f"[Device._login] Successfully connected to device")

日志目录结构

执行测试后,日志文件会按以下结构组织:

logs/
└── TestCases/
    └── 2026-06-02/                    # 日期目录(YYYY-MM-DD)
        └── test_npu_info_11-38-05/     # 用例名_时间目录(HH-MM-SS)
            └── test_npu_info.html      # HTML 报告文件

目录结构说明

  • TestCases: 测试用例日志根目录
  • 日期目录: 按执行日期组织(格式:YYYY-MM-DD
  • 用例子目录: 按用例名和执行时间命名(格式:用例名_HH-MM-SS
  • HTML 文件: 插件生成的测试报告,文件名与用例名一致

通过 logger.path 属性可以获取当前用例的日志归档目录路径,方便在测试用例中创建自定义文件。

日志级别:

级别 方法 说明
TRACE 5 logger.trace() 跟踪信息(最小级别)
INFO 20 logger.info() 一般信息
TC_START 21 logger.tc_start() 测试用例开始
TC_STEP 22 logger.tc_step() 测试步骤记录
PRE_SET 23 logger.pre_set() 前置设置
POST_SET 24 logger.post_set() 后置设置
CMD 25 logger.cmd() 命令日志
CMD_RESPONSE 26 logger.cmd_response() 命令响应日志
TC_END 27 logger.tc_end() 测试用例结束
WARN 30 logger.warning() 警告信息
FAIL 35 logger.fail() 测试失败
ERROR 40 logger.error() 错误信息

logger的默认输出目录为logs,级别默认为INFO,如果需要配置:

1、在pytest.ini中配置(推荐做法)

infra_log_dir = logs
infra_log_level = INFO

2、在执行命令时指定

pytest --testbed=testbed.json --infra_log_level=INFO ./testcase/test_demo.py::TestDemoExecution::test_single_device

用例环境模板使用

基本用法

包含一个json文件和两个py文件

# resource/device_desc/template/single_device_template.json
{
  "template_type": "single_device",
  "env_definition": {
    "nodes_1": [
      {
        "name": "device",
        "pool": "default",
        "label": "server"
      }
    ]
  }
}
# resource/mapper.py
from typing import Dict
ENV_TEMPLATES: Dict[str, str] = {
    "single_device": "resource/device_desc/template/single_device_template.json",
}

# test_case.py
from resource.mapper import ENV_TEMPLATES
@pytest.mark.env(template=ENV_TEMPLATES["single_device"])
def test_custom_attrs():
    assert True

模板结构说明

device_template/
├── template_type          #模版类型,模本的唯一性标识,请保证不同模版间值唯一
├── env_definition         #环境定义列表
│   ├── nodes_1            #第一套设备组合,代表用例需要覆盖的一种硬件形态,多套设备组合代表用例要覆盖多种硬件形态,会执行多次
        ├── device1        #设备1,设备详情字段见下表
        ├── device2        #设备2
│   ├── nodes_2            #第二套设备组合,一套设备组合中的设备必须是同一种类型(都为容器或都为裸机)
        ├── device
│   ├── ...

容器类型device字段说明:

字段名 含义 示例
name 设备名称标识,必填 "device_1"
res_type 资源类型,标识为容器,必填 固定值:"container"
cpu_arch CPU架构类型,非必填,默认值:ARM 可选值:"ARM"、"X86"
cpu_core_num CPU核数,非必填,默认值:1 1
npu_gen NPU代际版本,非必填,不填则此项无要求 "A2"
npu_model NPU具体型号,非必填,不填则此项无要求 "Ascend 910B4"
npu_num NPU数量,非必填,不填则无NPU 1
memory_size 内存大小,单位GB,非必填,默认值2 2
storage_opt_size 容器存储大小,单位GB,默认值40 40
softwares 软件列表,非必填,name: 软件名称,version:软件版本,config: 安装配置 [{"name":"cann","version":"8.0.T16","config":""}]
image_label 容器镜像,如果使用日构建镜像,此处可不填,由CI透传 "cann8.5.0-py3.11-torch2.9.0-torch_npu2.9.0rc1"
command 容器启动命令,非必填 ["/bin/bash", "-c", "python app.py"]
mount_source_data 需要挂载的共享存储数据源,非必填 ["deepseek_v4_pro", "baai_taco"]
mount_dest_dir 共享存储挂载的目的路径,按照索引和数据源对应,非必填 ["/mnt/ascend/models/deepseek_v4_pro", "/mnt/ascend/dataset/baai_taco"]

说明:
1、软件安装:需要业务自己提前写好安装脚本,提供给HidevLab归档后,申请环境时才可以安装
2、共享存储挂载:数据需要提前在共享存储服务器维护,并在HidevLab配置数据和存储路径的映射关系

容器类型环境模板示例:

{
    "name": "device_1",
    "res_type": "container",
    "cpu_arch": "ARM",
    "cpu_core_num": 1,
    "npu_gen": "A2",
    "npu_model": "Ascend 910B4",
    "npu_num": 1,
    "memory_size": 32,
    "storage_opt_size": 40,
    "softwares": [
      {
        "name": "cann",
        "version": "8.0.T16",
        "config": ""
      }
    ],
    "image_label": "swr.cn-north-4.myhuaweicloud.com/hw-test-images/ubuntu-310-sshd:latest",
    "command": ["/bin/bash", "-c","python app.py"],
    "mount_source_data": ["deepseek_v4_pro", "baai_taco"],
    "mount_dest_dir": ["/mnt/ascend/models/deepseek_v4_pro", "/mnt/ascend/dataset/baai_taco"]
}

裸机类型device字段说明:

字段名 含义 示例
name 设备名称标识 "device_2"
res_type 资源类型,标识为物理机,必填 固定值:"bms"
device_model 物理服务器设备型号,非必填,不填则此项无要求 "TS200 2280H V2"
cpu_arch CPU架构类型,非必填,默认值:ARM 可选值:"ARM"、"X86"
cpu_core_min_num 最小CPU核心数量要求,非必填,不填则此项无要求 1
npu_gen NPU代际版本,非必填,不填则此项无要求 "A2"
npu_model NPU具体型号,非必填,不填则此项无要求 "Ascend 910B4"
npu_min_num 最小NPU数量要求,非必填,不填则此项无要求 1
memory_min_size 最小内存容量(GB),非必填,不填则此项无要求 16
storage_min_size 最小存储容量(GB),非必填,不填则此项无要求 300
softwares 软件列表,非必填,name: 软件名称,version:软件版本,config: 安装配置 [{"name":"cann","version":"8.0.T16","config":""}]
os_release 操作系统版本信息,非必填,不填则此项无要求 CentOS 7.6
mount_source_data 需要挂载的共享存储数据源,非必填 ["deepseek_v4_pro", "baai_taco"]
mount_dest_dir 共享存储挂载的目的路径,按照索引和数据源对应,非必填 ["/mnt/ascend/models/deepseek_v4_pro", "/mnt/ascend/dataset/baai_taco"]

说明:
1、软件安装:需要业务自己提前写好安装脚本,提供给HidevLab归档后,申请环境时才可以安装
2、共享存储挂载:数据需要提前在共享存储服务器维护,并在HidevLab配置数据和存储路径的映射关系

裸机类型环境模板示例:

{
    "name": "device_1",
    "res_type": "bms",
    "device_model": "TS200 2280H V2",
    "cpu_arch": "ARM",
    "cpu_core_min_num": 1,
    "npu_gen": "A2",
    "npu_model": "Ascend 910B4",
    "npu_min_num": 1,
    "memory_min_size": 16,
    "storage_min_size": 300,
    "softwares": [
      {
        "name": "cann",
        "version": "8.0.T16",
        "config": ""
      }
    ],
    "os_release": "CentOS 7.6",
    "mount_source_data": ["deepseek_v4_pro", "baai_taco"],
    "mount_dest_dir": ["/mnt/ascend/models/deepseek_v4_pro", "/mnt/ascend/dataset/baai_taco"]
}

case_info 标记使用

pytest-testkit 插件支持通过 case_info 标记为测试用例添加自定义属性,用于用例分类和筛选。

基本用法

# 支持任意自定义属性
@pytest.mark.case_info(
    level="P1", type="Performance", owner="team1", module="auth", tags="perf"
)
def test_custom_attrs():
    assert True

用例筛选

在 pytest.ini 中配置筛选条件,只运行匹配的用例:

[pytest]

# 筛选 level=P1 且 module=auth 的用例
[case_info]
level = P1
module = auth

筛选逻辑说明

  • 精确匹配:ini 中配置的属性必须与用例属性精确匹配
  • 通配:ini 中未配置的属性不参与筛选(匹配所有)

示例

# 只筛选 level=P1 的用例
[case_info]
level = P1
# 筛选 level=P1 且 owner=team1 的用例
[case_info]
level = P1
owner = team1

error_log_fail 功能

pytest-testkit 插件支持当测试执行过程中出现 ERROR/WARN/FAIL 等级别日志时自动标记测试失败。

支持CLI 参数 和 ini 配置,默认为禁用

使用方式:

1、命令行参数(推荐用于临时启用)

pytest --error-log-fail --testbed=testbed.json testcase/

2、pytest.ini 配置(推荐用于项目级永久配置)

[pytest]
error_log_fail = true

使用示例:

# 开启error_log_fail功能后 用例结果为fail
def test_environment(logger):
    logger.error("this is a error log")
    assert True

quiet_stdout 功能

pytest-testkit 插件支持全局静默 sendcmd / sendcmd_interactive 的 stdout 输出,避免大量回显淹没日志。 开启后,stdout 内容不会出现在日志中,即使调用方显式传 echo_output=True 也会被全局压制。 exit_code、prompt、stderr 等信息不受影响。

支持CLI 参数 和 ini 配置,默认为禁用

使用方式:

1、命令行参数(推荐用于临时启用)

pytest --quiet-stdout --testbed=testbed.json testcase/

2、pytest.ini 配置(推荐用于项目级永久配置)

[pytest]
quiet_stdout = true

CI 运行元数据(ci_env_info)

执行器(pytest-executor)在调度用例时,会把 CI 流水线的运行元数据序列化成单个 ci_env_info 环境变量注入用例进程,覆盖 runner / remote / local 三种执行模式。 用例侧统一通过读取该环境变量获取当前流水线上下文,无需关心运行在哪种模式。

字段说明

环境变量 ci_env_info 的值是一个 JSON 字符串,固定包含以下白名单字段(值均为字符串):

字段名 含义 示例
project_name CI 项目名称 "openlibing/demo"
workflow_name 工作流名称 "nightly"
job_name 任务(job)名称 "run-pytest"
run_number 流水线运行序号 "128"
step_name 当前步骤名称 "pytest-orch"

读取方式

import json
import os

def test_use_ci_metadata():
    raw = os.environ.get("ci_env_info", "")
    info = json.loads(raw) if raw else {}
    job_name = info.get("job_name", "")
    run_number = info.get("run_number", "")
    # 例如按流水线序号归档产物、按 job 名分支断言逻辑
    assert isinstance(job_name, str)

取值约定

  • 白名单字段恒在:无论入参是否提供,解析后的 JSON 一定包含上表 5 个字段,缺省值为 ""
  • 值经过清洗:单个字段值会剔除 NUL、换行等控制字符,并按 128 字符截断; 非字符串标量(如数字)转为字符串,非标量(字典/列表)降级为 ""
  • 非法输入不阻断用例:入参结构不合法、缺失或类型错误时一律降级为空串并告警, 不会中断用例调度;未注入时 ci_env_info""
  • 纯 ASCII 安全:JSON 以 ensure_ascii 序列化,含中文等非 ASCII 值时以 \uXXXX 转义,避免经 SSH 传输后乱码。
  • 该变量由执行器注入,本地直接 pytest 调试时通常为空,用例代码应对空值做好兜底。

支持的 fixture

  • logger: 提供日志对象

    • info, warning, error等方法: 记录不同级别的日志
      • msg: 日志消息字符串

    日志记录示例:

    def test_environment(logger):
        logger.info("this is a info log")
        logger.warning("this is a waring log")
        logger.error("this is a error log")
    
    • set_header, set_footer: 设置 HTML 报告头部和尾部内容
      • html_content: 要注入的 HTML 内容
      • append: 是否追加到现有内容之后,默认覆盖

    HTML 内容注入示例:

    def test_example(logger):
        # 设置 HTML 报告头部内容(注入位置:统计信息头之后、详细信息表格之前)
        logger.set_header('<span class="custom-header">算子测试结果</span>')
    
        # 设置 HTML 报告尾部内容(注入位置:详细信息表格之后)
        logger.set_footer('<span class="custom-footer">测试完成</span>')
    
        # 多次注入内容默认覆盖,可通过 append=True 追加内容
        logger.set_header("<span>第一部分</span>", append=True)
        logger.set_header("<span>第二部分</span>", append=True)  # 结果:第一部分 + 第二部分
    
    • path: 获取当前用例的日志归档目录路径(只读属性)
      • 返回值: 当前用例的日志归档目录路径字符串,若未进入用例则返回 None

    获取日志目录路径示例:

    def test_example(logger):
        # 获取当前用例的日志归档目录路径
        log_dir = logger.path
        if log_dir:
            print(f"当前用例日志目录: {log_dir}")
            # 可以在此目录下创建自定义文件
            custom_file = os.path.join(log_dir, "custom_report.txt")
            with open(custom_file, "w") as f:
                f.write("自定义报告内容")
    
  • environment/environments: 提供单设备/多设备对象

    • sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True):执行命令(非交互式)

      • cmd: 要执行的命令字符串
      • timeout: 命令执行超时时间(秒)
      • env_vars: 环境变量字典
      • cwd: 工作目录,命令将在该目录下执行(可选)
      • ignore_err: 是否忽略命令执行错误,默认 False。设为 True 时即使命令失败 success 也为 True
      • only_stdout: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
      • 返回值: 默认返回 stdout 字符串;当 only_stdout=False 时返回 {'success': bool, 'stdout': str, 'stderr': str}
    • sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True):执行命令(交互式)

      • cmd: 要执行的命令字符串
      • expect_prompt: 期望匹配的提示符正则表达式(可选)
      • timeout: 命令执行超时时间(秒)。默认值600s
      • cwd: 工作目录,命令将在该目录下执行(可选)
      • ignore_err: 是否忽略提示符匹配失败,默认 False。设为 True 时即使未匹配到提示符 success 也为 True
      • only_stdout: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
      • 返回值: 默认返回 stdout 字符串;当 only_stdout=False 时返回 {'success': bool, 'stdout': str, 'stderr': str}
      • 输出清理: 返回的 stdout 会自动清理以下内容,便于用户直接使用:
        • CSI 控制序列(如 \x1b[32m 颜色代码、\x1b[?2004h bracketed paste mode 等)
        • OSC 控制序列(如 \x1b]0;title\x07 窗口标题设置等)
        • 命令回显及其之前的内容(包括 Login Banner)
        • 回车换行符统一转换为 \n\r\n\r 自动处理)
    • upload_file(local_file, remote_file):上传文件

      • local_file: 本地文件路径
      • remote_file: 远程文件路径
      • 返回值: 上传成功返回True,失败返回False
    • download_file(remote_file, local_file):下载文件

      • remote_file: 远程文件路径
      • local_file: 本地文件路径
      • 返回值: 下载成功返回True,失败返回False
    • reconnect():设备重连

      • 返回值: 重连成功返回True,失败返回False

    设备参数使用方法示例如下:

    # 单设备
    def test_environment(environment):
        my_env_dict = {"MY_VAR": "value"}
        output = environment.sendcmd("ls", timeout=5, env_vars=my_env_dict)
        print(output)  # 直接打印stdout字符串
    
        # 需要获取完整结果时
        result = environment.sendcmd("ls", only_stdout=False)
        assert result["success"]
        print(result["stdout"])
    
        # 在指定目录执行命令
        output = environment.sendcmd("pwd", cwd="/tmp")
    
        # 忽略命令执行错误并获取完整结果
        result = environment.sendcmd("some_command", ignore_err=True, only_stdout=False)
        # result['success'] 总是 True
    
        environment.upload_file("local.txt", "/remote/path/remote.txt")
        environment.download_file("/remote/path/remote.txt", "local.txt")
    
    
    # 多设备 device1_name为模板中name 其他用法与单设备相同 参考单设备示例
    def test_environments(environments):
        environment_device1 = environments["device1_name"]
        output = environment_device1.sendcmd("ls")
        # 需要检查执行结果时
        result = environment_device1.sendcmd("ls", only_stdout=False)
        if not result["success"]:
            print(f"Error: {result['stderr']}")
    
  • set_docker(docker_name_or_id):注册 Docker 容器(名称或 ID)

    • docker_name_or_id: 容器名称或容器 ID(Docker ID 为小写十六进制字符串)
    • 返回值: None
    • 功能: 注册设备上的 Docker 容器,注册后可通过 __getitem__ 获取容器代理对象
    • 查询顺序: 先尝试按 ID 查询,若找不到再尝试按名称精确查询
  • DockerProxy 容器代理对象:通过 environment["container_name"] 获取

    • 代理对象提供与 Device 一致的接口,自动包装 docker exec 命令

    • sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True):非交互式命令

      • 每次独立执行 docker exec {docker_name} {cmd}
      • 返回值: stdout 字符串或完整字典
    • sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True):交互式命令

      • 持久 session 模式:首次调用进入容器,后续调用在同一 session 执行
      • 只有交互式 shell 命令(bash/sh/python/mysql 等)才创建持久 session
      • 非交互式命令(cat/ls/grep 等)执行后自动退出,不设置 session 状态
      • 参数与 Device.sendcmd_interactive 相同
      • 返回值: stdout 字符串或完整字典
    • exit_docker(expect_prompt=None, timeout=3):退出容器 session

      • 执行 exit 命令退出容器,回到主机 shell
      • 若未进入 session 状态,打印 warning 日志并返回空字符串

    Docker 容器代理使用示例:

    # 单设备场景
    def test_with_docker(environment):
        # 在裸机上拉起容器
        environment.sendcmd("docker run -d --name my_container ubuntu:latest sleep 3600")
    
        # 注册容器
        environment.set_docker("my_container")
    
        # 获取容器代理对象
        container = environment["my_container"]
    
        # 非交互式命令(每次独立执行)
        output = container.sendcmd("ls /")
        assert "bin" in output
    
        # 进入容器交互式 session
        # 必须使用/bin/bash 、/bin/sh 等交互式 shell 命令才能进入交互式 session
        container.sendcmd_interactive("/bin/bash")
    
        # 在同一 session 中执行多条命令
        container.sendcmd_interactive("ls -la")
        container.sendcmd_interactive("cat /etc/hosts")
    
        # 退出容器交互式 session 非交互式无需调用
        container.exit_docker()
    
        # environment.sendcmd 仍执行裸机命令
        environment.sendcmd("docker ps")
    
    
    # 多设备场景
    def test_multi_device_with_docker(environments):
        dev1 = environments["device1"]
    
        # 在 device1 上拉起容器
        dev1.sendcmd("docker run -d --name app_container nginx:latest")
    
        # 注册容器
        dev1.set_docker("app_container")
    
        # 获取容器代理对象(两级字典取值)
        container = dev1["app_container"]
        output = container.sendcmd("nginx -v")
    
        # 非 shell 命令不会创建交互式 session
        container.sendcmd_interactive("cat /root/abc.txt")
        container.exit_docker()  # 打印 warning,不执行 exit
    
    
    # 通过容器 ID 注册(适合容器无名称的情况)
    def test_with_docker_by_id(environment):
        # 在裸机上拉起容器(不指定名称)
        environment.sendcmd("docker run -d ubuntu:latest sleep 3600")
    
        # 获取容器 ID
        output = environment.sendcmd("docker ps -q --filter ancestor=ubuntu:latest")
        container_id = output.strip()  # 例如 "a1b2c3d4e5f67890..."
    
        # 注册容器(通过 ID,支持短 ID 或完整 ID)
        # Docker ID 为小写十六进制字符串(至少需要能唯一标识容器)
        environment.set_docker(container_id[:12])  # 使用短 ID(12 位)
        # 或使用完整 ID: environment.set_docker(container_id)
    
        # 获取容器代理对象
        container = environment[container_id[:12]]
    
        # 执行命令
        output = container.sendcmd("ls /")
        assert "bin" in output
    
        # 清理容器
        environment.sendcmd(f"docker stop {container_id}")
        environment.sendcmd(f"docker rm {container_id}")
    

    注意

    • 交互式 shell 命令集合:bash、sh、ash、dash、zsh、python、python3、mysql、psql 等
    • 非交互式命令(cat/ls/grep 等)使用 sendcmd_interactive 后执行完毕自动退出容器,不会设置 session 状态
    • 进入交互式shell后需调用 exit_docker() 退出容器 session,非交互式无需调用
    • set_docker 支持容器名称和容器 ID,查询顺序:先尝试 ID 查询,失败后再尝试名称精确查询

前置动作注册

支持在测试执行前注册并执行前置动作,用于环境变量加载、参数注入、设备初始化等场景。

核心特性:

  • conftest.pypytest_runtest_setup 中通过 register_pre_action 注册前置动作
  • pytest_runtest_setup在每个用例执行前都会被调用
  • 前置动作在环境分配后、每个测试用例执行前自动执行
  • 执行失败时用例直接报错,报错信息包含失败动作名称和详细错误

使用方式:

conftest.py 中定义并注册前置动作:

import pytest
from pytest_testkit import (
    register_pre_action,
    clear_pre_action_registry
)

def load_env_var(environments):
    """在远程设备上设置环境变量"""
    device = list(environments.values())[0]
    result = device.sendcmd_interactive("export TEST_MODE=MY_ENV", timeout=5)
    return "set succ"

def wait_seconds(delay):
    """本地等待(第一个参数不是 environments,不注入设备)"""
    import time
    time.sleep(delay)
    return {"waited": delay}

@pytest.hookimpl(hookwrapper=True)  # 装饰器必选
def pytest_runtest_setup(item):
    # 可选  执行前置动作前清空历史注册的前置动作
    clear_pre_action_registry()
    # load_env_var函数第一个参数是 environments,注入设备字典
    register_pre_action(
        name="load_env_var",
        func=load_env_var,
    )
    # wait_seconds函数第一个参数不是 environments,不注入设备字典
    register_pre_action(
        name="wait_seconds",
        func=wait_seconds,
        args=[2]
    )
    # 必选  需放在register_pre_action后,可理解为调用插件的pytest_runtest_setup函数,执行前置动作
    yield

多个conftest.py调用顺序:

假设有两组用例分别在TestRepo/testsTestRepo/unittest下, conftest.py可选位置及pytest_runtest_setup的调用顺序举例如下

# 目录结构
TestRepo
├── conftest.py
├── tests/
│   └── conftest.py
└── unittest/
    └── conftest.py

# 以tests/下用例某个用例为例,
# pytest_runtest_setup先调用用例同级的conftest.py文件,再调用父级目录下的
TestRepo/tests/conftest.py
          ↓
TestRepo/conftest.py

register_pre_action 接口参数说明:

参数 类型 说明
name str 前置动作唯一标识
func Callable 执行函数
args list 传递给 func 的位置参数
kwargs dict 传递给 func 的关键字参数
forced bool 是否强制执行。默认为 False,相同name的前置动作在整个 session 中只执行一次;设置为 True 时,即使已执行过也会在每个测试用例前重新执行

函数签名规则:

系统通过检测函数第一个参数名判断是否注入 environments

  1. 第一个参数是 environments:系统自动注入设备字典,可使用environment提供的方法操作环境,如sendcmd
def my_action(environments, **kwargs):
    for name, device in environments.items():
        device.sendcmd("hostname")

register_pre_action(name="my_action", func=my_action)
  1. 第一个参数不是 environments:不注入设备字典
def local_action(delay, message):
    import time
    time.sleep(delay)
    print(message)
    return {"done": True}

register_pre_action(name="local_action", func=local_action, args=[1, "hello"])

错误处理:

  • Registry 未初始化:抛出 RegistryNotInitializedError
  • 执行失败:抛出 PreActionError,测试用例报错终止

执行测试

基本命令

# 指定 testbed 文件执行测试
pytest --testbed=./testbed.json testcase/

# 指定测试目录
pytest --testbed=./testbed.json tests/

# 运行指定测试
pytest --testbed=./testbed.json testcase/test_demo.py::TestDemoExecution::test_single_device

# 过滤测试用例(按 level 和 type)
pytest --testbed=./testbed.json testcase/ -v

# 生成 testbed 模板
pytest --collect-only --outtestbed=testbed.json

pytest.ini 配置

[pytest]
# 日志配置:目录、级别
infra_log_dir = logs
infra_log_level = INFO

# 错误日志失败配置:当检测到 ERROR/WARN/FAIL 日志时标记测试失败
error_log_fail = true

# 指定测试文件搜索路径(可提升收集速度,避免扫描无关目录)
testpaths = tests/demo

# 默认命令行选项(每次运行 pytest 时自动应用)
addopts =
    -v                          # 详细输出
    --tb=short                  # 精简 traceback
    -m smoke                    # 按自定义mark过滤

# 注册自定义 markers(避免使用未声明的 marker 产生警告)
markers =
    smoke: 标记冒烟测试用例
    ui: UI 自动化测试
    api: 接口测试

# 忽略某些文件或目录(可选)
; ignore =
;     tests/old/
;     tests/conftest_legacy.py

# 指定 Python 文件匹配模式
python_files = test_*.py

# 指定测试类和函数的命名规则
python_classes = Test*
python_functions = test_*

# 自定义用例筛选条件,对应用例的自定义mark中的case_info信息
[case_info]
level = P0
type = Functional

# 用例id列表筛选条件,使用用例函数名筛选,多个用例函数名使用英文逗号隔开不要带空格
[test_ids]
include = id1,id2
exclude = id3,id4

Dependencies

见requirements.txt

License

MIT License