
简介这是一套基于pytestyamlddtallure打造的接口自动化测试框架源码面向测试工程师与接口测试初学者可直接应用于公司项目的接口回归测试。框架源自码尚VIP课程经实际项目升级改造实测稳定可用使用者只需按规范录入接口用例数据即可快速接入现有测试流程降低自动化落地与维护门槛。压缩包共126个文件整体约1.02MB以JSON、YAML、Python源码三大类为主JSON用于测试数据与配置管理YAML承载用例编写规范py脚本实现核心测试逻辑与Allure报告集成另附带HTML报告模板、CSV测试套件配置、ini配置文件及相关辅助文件目录结构清晰便于二次开发与扩展。目前已有4341人学习下载对想系统掌握接口测试框架搭建、提升自动化实战能力的技术人员而言是一份高性价比的参考源码。1. 接口自动化框架pytestyamlddt 组合为什么是测试工程师的稳妥选择做接口测试的测试工程师早晚会面对同一个问题用例从几十条涨到几百条之后维护成本开始反噬改一个字段要翻十几个文件。python 生态里能扛住这个量级的方案不少但 pytest yaml ddt allure 这套组合是我在多个项目里反复对比后留到现在的配置yaml 把测试数据和代码拆开不懂代码的同事也能看懂用例在测什么ddt在 pytest 里实际由 parametrize 承接让一条用例逻辑跑遍多组数据不用复制粘贴allure 把结果渲染成可交付的报告给产品看、给开发看都拿得出手。这套框架解决的不是「能不能跑」而是「别人接手时还要不要骂人」。如果你正打算从零搭接口自动化或是在 unittest 老框架里挣扎下面按骨架搭建、数据驱动、请求封装、排错到报告落地的顺序完整过一遍每个环节都有能直接抄的代码和实际踩过的坑。2. 骨架搭建目录分层、conftest 全局钩子与统一入口2.1 目录结构分层约定决定三个月后的维护体验我见过不少接口自动化项目前期跑得欢三个月后没人敢动——公共方法散落在各个用例文件里换环境要改八个文件加一条用例要问写代码的人「这个函数在哪」。搭这套框架的第一步不是写用例而是把目录结构定死。下面这个结构是我在多个项目里调整后的版本按「配置、数据、公共、用例、报告」五块切分api_auto/ ├── config/ │ ├── config.yaml # 环境地址、账号、超时等全局配置 │ └── config_reader.py # 配置读取模块 ├── data/ │ ├── login.yaml # 按模块拆分的 yaml 用例数据 │ └── order.yaml ├── testcases/ │ ├── conftest.py # 全局 fixture 与钩子 │ ├── test_login.py # 用例入口只负责组装 │ └── test_order.py ├── common/ │ ├── request_client.py # requests 会话封装 │ ├── assert_utils.py # 断言工具 │ ├── context.py # 用例间数据上下文 │ ├── data_loader.py # yaml 加载与校验 │ └── log_utils.py # 日志封装 ├── report/ # allure 报告输出目录git 忽略 ├── logs/ # 运行日志目录git 忽略 ├── run.py # 统一入口脚本 └── requirements.txt这套分层的逻辑很简单config 管环境data 管数据common 管公共能力testcases 只放用例。testcases 里的文件尽量薄一个用例文件只做三件事——取 yaml 数据、调请求封装、断言结果。我在实际项目里还加了两条硬性约定第一common 目录下不允许 import testcases 里的任何东西防止循环依赖第二report 和 logs 必须进 .gitignore因为 allure 原始结果一次运行就是几十 MBCI 跑几轮仓库直接膨胀。requirements.txt 里我会锁住这几项pytest、pyyaml、requests、allure-pytest、pytest-rerunfailures、jsonpath。前五个是框架主力jsonpath 是给断言层用的后面会展开。这里不建议装各种「全自动」的第三方接口测试封装库——接口测试的请求差异太大通用封装往往在第一个特殊接口上就翻车自己写的薄封装反而可控出问题你知道去哪改。2.2 conftest.pyfixture 与全局钩子放一处管conftest.py 是 pytest 的钩子集中地。很多新手把 fixture 写在单个用例文件里结果换环境时要改一堆文件换个 fixture 作用域要找半天。我的习惯是只放根目录的 conftest.py全局统一管理。# testcases/conftest.py import os import pytest import yaml BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) pytest.fixture(scopesession) def env_config(): 读取 config/config.yaml按 TEST_ENV 返回对应环境配置 env_name os.getenv(TEST_ENV, test) config_path os.path.join(BASE_DIR, config, config.yaml) with open(config_path, r, encodingutf-8) as f: all_config yaml.safe_load(f) if env_name not in all_config: raise ValueError(fconfig.yaml 中没有环境 [{env_name}] f可用环境: {list(all_config.keys())}) return all_config[env_name] pytest.fixture(scopesession) def api_client(env_config): 全局唯一的请求会话带基础地址与超时 from common.request_client import RequestClient return RequestClient( base_urlenv_config[base_url], timeoutenv_config.get(timeout, 10) ) pytest.fixture(autouseTrue) def log_case_boundary(request): 每条用例前后打日志边界方便在几百条日志里定位 import logging logging.info( 开始: %s , request.node.name) yield logging.info( 结束: %s , request.node.name)这段代码的关键在 fixture 作用域env_config 和 api_client 用 scopesession整个测试会话只初始化一次。如果你的接口测试涉及登录 token 过期刷新建议在 api_client 内部做 token 的惰性刷新而不是改成 function 作用域让每条用例新建 session——几百条用例光 TLS 握手就能拖慢一两分钟这在回归里是完全没必要的开销。TEST_ENV 是切换测试、预发、生产环境的开关config.yaml 里按环境名分块存 base_url、账号、超时等。env_config 里找不到环境名时直接抛 ValueError而不是返回空字典让后面的用例在奇怪的地方崩溃——失败要尽早、信息要明确。autouseTrue 的 log_case_boundary 不需要显式引用pytest 自动在每条用例前后执行request.node.name 是当前用例名打这两行边界日志配合后面的落盘日志能快速圈定一条用例的执行区间。2.3 run.py 统一入口环境切换与参数透传用例文件可以直接 pytest 命令跑但团队协作时需要统一入口尤其要支持指定环境和指定模块。我一般写一个轻量 run.py# run.py import argparse import os import subprocess import time def main(): parser argparse.ArgumentParser(description接口自动化统一入口) parser.add_argument(--env, defaulttest, choices[test, pre, prod], help运行环境) parser.add_argument(--module, default, help指定模块文件如 test_order) parser.add_argument(--reruns, default1, help失败重跑次数) args parser.parse_args() os.environ[TEST_ENV] args.env timestamp time.strftime(%Y%m%d_%H%M%S) results_dir freport/allure-results_{timestamp} cmd [pytest, -s, -v, --alluredir, results_dir, --reruns, args.reruns] if args.module: cmd.append(ftestcases/{args.module}.py) else: cmd.append(testcases) print(执行命令:, .join(cmd)) subprocess.run(cmd, checkTrue)这段脚本的核心是 checkTruepytest 返回非零退出码时 subprocess 直接抛异常脚本以失败状态退出CI 才能正确判定本次回归是红是绿。结果目录带时间戳是为了避免下一次运行把上一次的 allure 原始结果覆盖掉——allure 的 results 目录一旦混入新旧数据报告里就会出现重复用例和残留失败这个属于「看起来过了实际数据是脏的」的典型问题。参数说明--reruns 透传给 pytest-rerunfailures 插件偶发的网络抖动导致的失败会自动重跑一次。重跑次数别设太高我一般设 1如果设 3 还挂那基本是断言写错或接口真挂了重跑只是在掩盖问题。3. yaml 驱动用例数据与代码分离加新用例不动 py 文件3.1 yaml 用例格式请求、预期、提取三层字段设计数据驱动最核心的问题是「一条用例长什么样」。我最早用 Excel 管理用例后来发现 Excel 的行列结构表达接口请求很别扭——请求头、参数、预期结果是天然的层级关系硬压成表格反而丢失信息。yaml 的嵌套结构刚好匹配而且不用装额外依赖pyyaml 一读就完事。一个模块的 yaml 用例我通常这么写# data/order.yaml - name: 创建订单-正常流程 method: POST url: /api/v1/order/create headers: Content-Type: application/json params: user_id: 1001 sku_id: SKU_2024_001 quantity: 2 expected: code: 0 message: success data: order_id: ORD_2024_001 extract: order_id: $.data.order_id - name: 创建订单-库存不足 method: POST url: /api/v1/order/create headers: Content-Type: application/json params: user_id: 1002 sku_id: SKU_2024_002 quantity: 999 expected: code: 10001 message: 库存不足这段 yaml 定义了三个层次请求层method、url、headers、params、预期层expected、关联层extract。expected 只做「业务码 关键字段」的校验不要把整个响应体都抄进 yaml——响应里的时间戳、随机订单号、服务端版本号会把你逼疯今天写死的值明天就变最后只能天天改用例。extract 字段是这套设计里比较实用的部分接口测试经常要拿上一个接口的返回值订单号、token传给下一个接口。用 JSONPath 表达式提取后存入全局上下文后续用例用 ${order_id} 占位符引用用例之间是显式依赖而不是靠全局变量乱穿。命名上我要求 name 字段必须是「模块-场景-预期结果」三段式比如「创建订单-正常流程-返回成功」这样报告里一眼能看出在测什么。3.2 参数化接入ddt 在 pytest 里的正确打开方式很多从 unittest 转过来的测试工程师会纠结 ddt 库怎么用。这里有个值得说清楚的结论在 pytest 里ddt 库本身几乎用不上因为 pytest.mark.parametrize 已经覆盖了同样的能力而且和 fixture、allure 的兼容性更好。ddt 的 ddt、data、unpack 写法在 pytest 下反而多余还会导致 allure 报告里用例名显示成乱码。我的做法是写一个数据加载函数把 yaml 和 parametrize 串起来# common/data_loader.py import os import yaml def load_yaml_cases(file_name): 读取 data 目录下的 yaml 文件返回过滤后的用例列表 base_dir os.path.dirname(os.path.dirname(os.path.abspath(__file__))) path os.path.join(base_dir, data, file_name) with open(path, r, encodingutf-8) as f: cases yaml.safe_load(f) if not isinstance(cases, list): raise TypeError(f{file_name} 的根节点必须是列表) # 过滤掉没有 name 的条目防止误写导致静默跳过 return [case for case in cases if case.get(name)]# testcases/test_order.py import allure import pytest from common.data_loader import load_yaml_cases allure.feature(订单模块) class TestOrder: pytest.mark.parametrize(case, load_yaml_cases(order.yaml), idslambda c: c[name]) def test_create_order(self, api_client, case): resp api_client.request( methodcase[method], urlcase[url], headerscase.get(headers, {}), paramscase.get(params, {}), extractcase.get(extract, {}) ) body resp.json() assert body.get(code) case[expected][code]这里要留意 ids 参数idslambda c: c[name] 的作用是把参数化用例在报告里的名字从「case0、case1」替换成 yaml 里定义的业务名。很多人报告难看就是漏了这行。parametrize 的第二个参数是可迭代对象列表里每个元素都会生成一条独立用例配合 load_yaml_cases 的过滤逻辑可以做到「yaml 里加一条数据代码一行不改」。参数说明case.get(headers, {}) 用了 get 加默认值因为不是每条用例都要自定义请求头。yaml 里没写的字段在代码里用 get 兜底否则哪天有人漏写一个字段整个用例文件直接崩而报错信息还指向代码而不是缺字段的那条数据。3.3 用例间数据关联extract 提取与占位符替换接口之间有依赖是常态登录拿 token下单拿订单号支付用订单号。我的方案是一个轻量上下文对象 请求封装里的占位符替换。先看上下文# common/context.py class Context: 用例间共享数据的上下文对象 def __init__(self): self._data {} self._owner {} # 记录每个 key 由哪条用例写入 def set(self, key, value, owner): self._data[key] value self._owner[key] owner def get(self, key): if key not in self._data: raise KeyError( f上下文里没有 [{key}]请检查写入用例是否执行成功) return self._data[key]然后在 RequestClient 里加一个渲染函数把 url 和 params 里的 ${xxx} 占位符替换成上下文值# common/request_client.py 内新增 import re def render(self, template, context): 把 ${key} 形式的占位符替换为上下文里的值 def replace(match): key match.group(1) return str(context.get(key)) return re.sub(r\$\{(\w)\}, replace, template)逻辑说明请求发出前对 url、params 的每个字符串值调用 render。这样 case[url] 写成 /api/v1/order/detail/${order_id}执行时自动替换成上一次用例提取的真实订单号。context.get 在 key 不存在时抛 KeyError并提示「检查前置用例是否执行」——这个设计是有意的比返回 None 再在请求里拼出个 None 字符串强得多后者排查起来极其痛苦。参数说明正则 r${(\w)} 只匹配字母、数字、下划线组成的占位符如果你的变量名里带连字符或点需要扩展这个正则。我在项目里遇到过 token 里带点号的情况最后约定所有上下文 key 一律用下划线命名从源头避免这类问题。4. 请求封装与断言设计把「黑匣子」拆成可定位的现场4.1 requests.Session 封装超时、重试与鉴权头裸用 requests 写接口用例的最大问题不是跑不通而是每次失败都要人肉去看响应。几百条用例跑下来总有几个是网络抖动导致的偶发失败没有重试机制就得手动重跑。我的习惯是把会话、超时、重试、日志全部收敛在一个文件里# common/request_client.py import logging import time import requests from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter logger logging.getLogger(api_client) class RequestClient: def __init__(self, base_url, timeout10, max_retries1): self.base_url base_url.rstrip(/) self.timeout timeout self.session requests.Session() retry Retry(totalmax_retries, connect2, backoff_factor0.5, status_forcelist[502, 503, 504]) adapter HTTPAdapter(max_retriesretry) self.session.mount(http://, adapter) self.session.mount(https://, adapter) def request(self, method, url, headersNone, paramsNone, dataNone, jsonNone, extractNone): full_url self.base_url url if url.startswith(/) else url logger.info(请求: %s %s, 参数: %s, method, full_url, params) start time.time() try: resp self.session.request( methodmethod, urlfull_url, headersheaders, paramsparams, datadata, jsonjson, timeoutself.timeout, verifyFalse) elapsed round(time.time() - start, 3) logger.info(响应: %s, 耗时 %ss, 内容前200字符: %s, resp.status_code, elapsed, resp.text[:200]) return resp except requests.exceptions.Timeout: logger.error(请求超时: %s, full_url) raise except requests.exceptions.RequestException as e: logger.error(请求异常: %s, 错误: %s, full_url, e) raise def set_auth_token(self, token): 登录后调用后续所有请求自动携带 token self.session.headers.update( {Authorization: fBearer {token}})逻辑说明session 对象会复用底层 TCP 连接比每次 requests.get 重新握手快很多跑几百条用例时差距非常明显。Retry 的 backoff_factor0.5 表示重试等待按 0.5 × 2^n 递增即 0.5 秒、1 秒、2 秒避免服务端刚重启时立刻重试又立刻失败。status_forcelist 里我只放了 502/503/5044xx 不重试——4xx 是参数问题重试多少次都一样只会浪费时间。参数说明verifyFalse 是为了跳过 HTTPS 证书校验测试环境用自签名证书很常见。但这条在生产环境必须关掉我在实际项目里用环境变量控制os.getenv(API_VERIFY) 1 时才 verifyTrue绝不在代码里写死。set_auth_token 是登录用例跑完后统一调用的比每条用例手动塞 token 头省心得多。4.2 断言三层业务码、字段存在性与 JSONPath 取值断言写不好框架就废了一半。我见过两种极端一种是什么都不断言接口返回 200 就算过接口返回错误信息也能「通过」另一种是把整个响应体复制进 yaml一跑全是时间戳差异。合适的做法是分层断言——业务码必查关键字段按需查嵌套值用 JSONPath 查。# common/assert_utils.py import jsonpath def assert_code(resp, expected_code): 校验业务码resp 是 requests.Response body resp.json() actual body.get(code) assert actual expected_code, \ f业务码不符: 期望 {expected_code}, 实际 {actual}, \ f响应前500字符: {resp.text[:500]} def assert_contains(body, required_keys): 校验关键字段存在required_keys 是列表 missing [k for k in required_keys if k not in body] assert not missing, f响应缺少字段: {missing} def assert_json_path(body, json_path, expected_value): 用 JSONPath 断言嵌套字段值 values jsonpath.jsonpath(body, json_path) assert values, fJSONPath 未匹配到任何值: {json_path} assert expected_value in values, \ f字段值不符: 期望 {expected_value}, 实际 {values}这套断言的使用逻辑是assert_code 管「接口通不通」assert_contains 管「结构对不对」assert_json_path 管「具体值准不准」三个函数覆盖 90% 的接口断言场景。jsonpath 库处理嵌套字典比手写循环遍历干净得多一条 $.data.order_list[0].sku_id 就能定位到任意深度的字段。这里有个使用习惯值得说断言失败时的报错信息必须带「期望 vs 实际」。上面的 f-string 把两个值都打出来了assert_code 里还把响应体截断 500 字符拼进失败信息这样报错本身就是一个完整的现场不用再翻日志。我在项目里甚至把这条拼进 CI 的失败摘要开发同学点开一眼就能定位。4.3 日志落盘与轮转跑完知道发生了什么接口自动化最崩溃的场景是跑完 200 条用例挂了 3 条但谁都不知道挂的时候请求发了什么、响应回了什么。控制台日志会滚动刷掉所以日志必须落盘。# common/log_utils.py import logging import os from logging.handlers import RotatingFileHandler def setup_logger(nameapi_auto, log_dirlogs): os.makedirs(log_dir, exist_okTrue) logger logging.getLogger(name) if logger.handlers: return logger logger.setLevel(logging.INFO) fmt logging.Formatter( %(asctime)s [%(levelname)s] %(name)s: %(message)s) fh RotatingFileHandler( os.path.join(log_dir, run.log), maxBytes5 * 1024 * 1024, backupCount3, encodingutf-8) fh.setFormatter(fmt) logger.addHandler(fh) return logger这段封装的关键是 RotatingFileHandler日志文件到 5MB 自动切成 run.log.1、run.log.2保留最近 3 个文件不会把磁盘写爆。接口自动化回归一跑就是几百条请求日志不轮转的话一个月能攒几个 GBCI 机器磁盘满的坑就是这么来的。encodingutf-8 也很关键——Windows 上不指定的话接口返回里带中文直接抛 UnicodeEncodeError整个测试会话在日志阶段就中断。每次 request 调用里打的那条「响应前200字符」日志是我定位问题的第一入口不用开 allure直接翻日志就能看到当时的请求参数和响应片段。5. 避坑与排查五个高频翻车点直接对号入座5.1 yaml 缩进与编码ParserError 和中文乱码现象yaml.safe_load 抛 yaml.parser.ParserError报错位置指向一个看起来完全正常的行或者用例里的中文在请求里变成乱码。原因yaml 对缩进极度敏感tab 和空格混用必挂而且报错行往往不是真正的出错行是解析器「发现不对劲」的位置。另一个是文件打开时没指定 encodingutf-8Windows 默认用 GBK 读取中文全部变成乱码。解决编辑器统一把 tab 替换成两个空格所有打开 yaml 的地方强制加 encodingutf-8。我在项目里加了一个加载前预检函数逐行扫描 tab 字符有就直接报「第几行存在 tab」——早失败比跑到一半失败好def _check_no_tab(path): for i, line in enumerate(open(path, r, encodingutf-8), 1): if \t in line: raise SyntaxError(f{path} 第 {i} 行存在 tab请替换为空格)5.2 参数化用例名乱码ids 忘写或数量不对现象allure 报告里用例名显示成 case[0]、case[1]或者中文名变成 \uXXXX 转义序列。原因parametrize 没写 ids 参数pytest 默认用索引编号或者 ids 函数返回的类型不是字符串pytest 序列化时就用了转义。ids 的数量和参数列表长度不一致时 pytest 会直接报错但这种报错信息很绕新手经常看不懂。解决idslambda c: c[name] 必须写而且保证 yaml 里的 name 字段是字符串。我在 data_loader 里加了校验name 为空、重复、或类型不是 str 都直接抛异常。重复的用例名在报告里会让人分不清谁是谁这个校验在数据量大了之后特别值钱。5.3 allure 报告里看不到请求和响应现象报告只显示断言结果红绿想看请求参数、响应体时发现全是空的。原因allure-pytest 默认只收集 pytest 的断言与运行信息requests 库的调用不会自动挂到 allure 报告上。很多人以为装了 allure-pytest 就万事大吉忘了请求层和报告层之间没有自动桥接。解决在 RequestClient.request 里加 allure 挂钩每次请求把 method、url、params、响应体 attach 到报告import allure def _attach_to_allure(self, method, url, params, resp_text): allure.attach( bodyf{method} {url}\nparams{params}, name请求信息, attachment_typeallure.attachment_type.TEXT) allure.attach( bodyresp_text[:2000], name响应信息, attachment_typeallure.attachment_type.TEXT)这样报告就不只是一个红绿矩阵每条用例都能点开看完整请求响应定位问题基本不用再翻日志。注意响应体截断 2000 字符避免大响应体把报告撑爆。5.4 用例间数据串场上下文数据被污染现象单独跑某条用例是过的整套跑就挂而且挂的用例和前面的用例存在数据关联。重跑一次可能又过了属于典型的「随机的偶发失败」。原因extract 提取的字段存成了全局字典后面的用例如果用了同一个 key会被前一条用例覆盖或者前一条用例跑失败时没写值后一条拿到的是上一次运行残留的旧数据。这种脏数据问题最难查因为报错信息看起来是「数据不对」实际是「数据来源不对」。解决上下文变量分两级的思路——session 级整个测试会话共享和用例级每条用例结束后清理。实现上在 Context 里记录来源 ownerset 时覆盖同 key 要先告警失败用例的 extract 值不写入上下文防止脏数据传给后续用例。另外在依赖方加防御性检查order_id context.get(order_id) if order_id is None: raise RuntimeError(前置用例未成功order_id 为空本次调用终止)5.5 代理环境变量与证书校验干扰现象用例在本地跑得好好的到 CI 机器上大面积超时或者所有 https 请求突然报证书校验失败。原因CI 环境里配置了系统代理环境变量HTTPS_PROXY / HTTP_PROXYrequests 默认会读取并走代理而代理对测试域名的访问策略很复杂超时、拦截都可能出现。另有 CI 机器上的 CA 信任库和本地不一样自签名证书直接校验失败。解决RequestClient 初始化时显式设置 session.trust_env False让 requests 忽略环境代理verify 参数用环境变量控制而不是写死。这个坑当年在迁移 CI 时卡了我半天最后在 conftest 里打印 session.trust_env 的值才发现是代理在捣鬼。从那以后我在 conftest 的 session 级 fixture 里固定打一行诊断日志记录「当前是否走代理、verify 值、目标环境地址」环境问题一眼就能识别。6. 从「能跑」到「能交付」allure 动态标题与验证三连6.1 动态标题和严重级别让报告替你说人话参数化用例在 allure 报告里默认会带上参数化编号可读性差。除了 parametrize 的 ids还需要在用例内部用动态标题把业务名重新挂一遍——尤其当 ids 和实际执行结果发生错位时动态标题是最终的展示结果class TestOrder: pytest.mark.parametrize(case, load_yaml_cases(order.yaml), idslambda c: c[name]) def test_create_order(self, api_client, case): allure.dynamic.title(case[name]) allure.dynamic.severity( allure.severity_level.CRITICAL if case.get(level, P1) P0 else allure.severity_level.NORMAL) # 后续请求与断言省略逻辑说明allure.dynamic.title 在用例执行阶段覆盖报告里显示的标题——参数化传入的英文名、编号都会被这个中文业务名取代。severity 级别我习惯用 yaml 里的 level 字段控制P0 级别的用例核心链路在报告里标记为 CRITICAL开发看一眼严重级别就知道哪些失败必须当天修。feature、story 这些装饰器放在类和方法上粒度是「模块-场景」和 yaml 的 name 三段式互相印证。6.2 本地验证三连清缓存、跑回归、看报告从「能跑」到「能交付」我给自己定了一个强制验证流程每次动完框架代码必须先走完这三步才允许提交第一步清掉上次的 allure 结果目录和日志目录。allure 的 results 目录不清会导致新旧数据混在一起报告里出现幽灵用例——这是最常见的「报告看起来不对」的原因。第二步用统一入口跑全量回归python run.py --env test跑完看退出码和失败用例数。第三步生成并打开报告allure serve 直接起本地服务看结果重点看失败用例的请求响应 attach 是否完整以及是否有重跑后通过但耗时异常的用例。这套流程其实就是在执行前先处理「脏数据」执行时看「退出码」执行后看「报告细节」三个动作。如果直接跑 pytest 命令跳过 run.pyTEST_ENV 环境变量可能没设对用例跑在了错误的环境上结果全红还不知道为什么。从那以后我每次动框架代码都强制走一遍这个三连——这一步省下来的排查时间比写框架本身还多。希望帮到你。本文还有配套的精品资源点击获取