1. 为什么我又把 pytest 脚本删了重写
写 pytest 的人大概都经历过这个循环:接口文档更新一版,测试脚本跟着改一轮;UI 改了个按钮文案,定位器全挂;CI 上跑出来的失败,一半是用例本身写错了断言。我统计过自己上一个项目,纯手写 pytest 用例的维护成本,大概占了整个测试工作量的六成以上,真正用来设计测试场景的时间反而被压缩得很少。
这篇要聊的解法是:把测试设计从代码里抽出来,写进 Markdown spec,让生成器读 spec 直接产出可运行的 pytest 脚本,覆盖 API、UI、CI 三类场景。核心检索词就三个——pytest、Markdown spec、自动生成。适合谁?适合已经在用 pytest 但被重复劳动拖住的测试同学,也适合想把测试接进 CI 但不知道从哪下手的后端/全栈。
整条链路里,模型调用这一环我用 TaoToken 统一收口:一个 Key、一个 API 通道,本地生成脚本时用它做复杂用例补全,CI 里也能复用同一套配置,不用在多个平台之间来回切。下面按「问题场景 → TaoToken 前置 → 可复制配置 → 验证跑通 → 排错 → 下一步」的顺序讲,每一步都能直接抄。
2. TaoToken 前置:把模型调用收成一个通道
2.1 为什么测试链路也需要统一 Key
Markdown spec 生成 pytest 有两种模式:纯模板展开(不调模型)和 AI 辅助补全(调模型)。前者快,后者能处理复杂业务逻辑。问题在于,AI 辅助那一步如果 Key 散落在各个工具里,CI 上就会变成一堆环境变量地狱。
TaoToken 在这里的角色是「统一入口」:官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址 https://taotoken.net/api 。你只需要在配置里写一次 base_url 和 api_key,本地脚本、Cursor、CI runner 全部复用。
2.2 拿 Key 与选通道
登录后进控制台,在 API Keys 页面创建一个 Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建时建议按用途命名,比如pytest-spec-local和pytest-spec-ci,方便后面在 CI secret 里区分。
如果你只是想让 spec 生成器补全用例,用模型对话通道就够:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果你打算把生成 + 执行 + 修复做成长期跑的 Agent 流程,那 Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
注意:Key 只存在本地
.env或 CI secret 里,不要写进 spec 文件,spec 是要提交到 Git 的。
3. 可复制配置:config.toml 与 settings.json 骨架
3.1 config.toml:生成器读的主配置
生成器需要一个配置文件告诉它「模型通道在哪、spec 目录在哪、输出到哪」。下面这份可以直接复制,改掉路径即可:
# config.toml [model] provider = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读,不硬编码 model = "claude-sonnet" # 按你控制台可用的模型名填 timeout = 60 [spec] input_dir = "./specs" output_dir = "./tests/generated" default_kind = "api_pytest" [runner] pytest_args = ["-v", "--tb=short"] base_url_env = "TEST_BASE_URL" [ci] report_dir = "./reports" upload_artifact = true关键点:api_key_env指向环境变量名,而不是直接写 Key。这样本地和 CI 用同一份 config.toml,只是环境变量来源不同。
3.2 settings.json:给编辑器/Agent 用的通道配置
如果你在 Cursor 或类似编辑器里让 AI 读 spec 生成脚本,需要一个 settings.json 告诉它走哪个通道:
{ "taotoken": { "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "defaultModel": "claude-sonnet", "endpoints": { "chat": "/v1/messages", "models": "/v1/models" } }, "pytest": { "specDir": "./specs", "generatedDir": "./tests/generated", "baseUrlEnv": "TEST_BASE_URL" } }两份配置的分工:config.toml 给本地生成脚本和 CI 用,settings.json 给编辑器里的 AI 辅助用。两者共享同一个TAOTOKEN_API_KEY环境变量,不重复维护。
3.3 环境变量落地
# 本地 export TAOTOKEN_API_KEY="你的Key" export TEST_BASE_URL="http://localhost:8899" # CI 里放到 repository secret,名字保持一致4. spec 示例文件与生成后的 pytest
4.1 API spec:一个文件描述一组用例
spec 就是带 YAML frontmatter 的 Markdown。下面这份描述用户接口的冒烟测试:
--- kind: api_pytest base_url: http://localhost:8899 cases: - id: TC-001 method: GET path: /api/users expect_status: 200 - id: TC-002 method: POST path: /api/users json_body: name: "张三" email: "zhangsan@example.com" expect_status: 201 --- # 用户接口冒烟测试 覆盖列表查询与创建两个核心路径。生成命令:
python codegen/run_spec.py specs/api_smoke.md --config config.toml产出的 pytest 脚本大致长这样:
# Auto-generated by spec runner import os import pytest import requests BASE_URL = os.environ.get("TEST_BASE_URL", "http://localhost:8899") def test_tc_001(api_session): """TC-001: GET /api/users -> 200""" r = api_session.get(BASE_URL.rstrip("/") + "/api/users", timeout=30) assert r.status_code == 200, r.text[:800] def test_tc_002(api_session): """TC-002: POST /api/users -> 201""" r = api_session.post( BASE_URL.rstrip("/") + "/api/users", json={"name": "张三", "email": "zhangsan@example.com"}, timeout=30, ) assert r.status_code == 201, r.text[:800]4.2 UI spec:用语义定位代替 CSS 选择器
UI 测试最容易挂的就是定位器。spec 里用 label/role 这类语义定位,生成出来的 Playwright 脚本稳定性会好很多:
--- kind: ui_pytest_playwright base_url: http://localhost:5569 steps: - action: goto path: /login - action: fill l1_label: "用户名" value: "admin" - action: fill l1_label: "密码" value: "admin123" - action: click l1_role: button l1_name: "登录" expect: - type: url_regex pattern: "/(dashboard|home)$" --- # 登录流程测试生成后执行:
pytest tests/generated/test_ui_login.py -v4.3 复杂用例:让模型补边界场景
模板展开只能覆盖你写进 spec 的用例。边界值、异常分支这类,可以让模型读 spec 后补全。这一步走 TaoToken 的模型对话通道,配置已经在 config.toml 里了:
python codegen/run_spec.py specs/api_smoke.md \ --config config.toml \ --enrich \ --prompt "补充余额不足、重复提交、超时三类异常用例"生成器会把 spec 内容 + 提示词发给 TaoToken,拿回补充后的用例再展开成 pytest。你可以在模型对话页面先手动试一下提示词效果:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
5. 验证请求与成功结果
5.1 本地跑通
# 1. 生成 python codegen/run_spec.py specs/api_smoke.md --config config.toml # 2. 执行 pytest tests/generated/test_api_smoke.py -v # 期望输出 # tests/generated/test_api_smoke.py::test_tc_001 PASSED # tests/generated/test_api_smoke.py::test_tc_002 PASSED # ===== 2 passed in 0.84s =====5.2 验证模型通道是否通
生成器调模型那一步如果失败,先单独验证通道:
curl -s https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" | head -c 400返回模型列表就说明 Key 和通道都正常。如果这里就报 401,问题在 Key;如果报连接错误,检查 base_url 有没有写错。
5.3 CI 触发
GitHub Actions 里最小可用的一段:
name: pytest-spec-ci on: push: branches: [main] paths: ["backend/**", "specs/**"] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - run: pip install PyYAML pytest requests pytest-playwright - run: playwright install chromium - run: python codegen/run_spec.py specs/api_smoke.md --config config.toml env: TAOTOKEN_API_KEY: ${{ secrets.TAOTOKEN_API_KEY }} - run: pytest tests/generated -v env: TEST_BASE_URL: http://localhost:8899提交一次代码,Actions 页面能看到 pytest 输出,全绿即链路通。
6. 本篇常见错排查
6.1 生成脚本报 Key 缺失
现象:TAOTOKEN_API_KEY not found。原因通常是 config.toml 里写了api_key_env但环境变量没导出。检查echo $TAOTOKEN_API_KEY是否有值。CI 里确认 secret 名字和 config.toml 里写的一致。
6.2 pytest 收集不到用例
现象:no tests ran。多半是生成目录不在 pytest 的收集路径里。检查pytest.ini或pyproject.toml里的testpaths,把tests/generated加进去。另一个可能是文件名没以test_开头,生成器默认会加前缀,如果你手动改了文件名要注意。
6.3 UI 测试定位超时
现象:Timeout waiting for locator。先确认base_url指向的服务真的起来了,再确认 label 文案和页面完全一致(包括空格)。语义定位对文案敏感,文案改了 spec 也要改。如果页面是异步渲染,在 spec 的 step 里加等待动作,而不是在生成后的脚本里手改。
6.4 CI 里模型调用超时
现象:本地能生成,CI 卡在 enrich 步骤。CI runner 网络出口和本地不同,先跑 5.2 的 curl 验证。如果 curl 通但生成器超时,把 config.toml 的timeout调大,或者把 enrich 拆成单独一步、失败不阻塞主流程。
6.5 生成脚本覆盖了手写用例
现象:重新生成后手写的补充用例没了。生成目录和手写目录要分开,tests/generated只放生成产物,手写用例放tests/manual。CI 里两个目录都跑,互不干扰。
7. 下一步:把生成链路接进长期流程
到这一步,你已经有了:Markdown spec → pytest 脚本 → 本地跑通 → CI 触发 的完整闭环。接下来两个方向可以按需选。
如果你主要想让模型帮你持续补用例、修失败断言,把生成 + 执行 + 修复做成一个能长期跑的 Agent 流程,用 Coding Plan 更省心:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它适合这种「反复调用、上下文要延续」的场景。
如果你只是想先把接入细节吃透,比如 Key 怎么管、base_url 怎么配、不同模型的 endpoint 差异,直接翻接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。文档里有各语言的调用示例,对着改 config.toml 就行。
最后给一个我踩过的坑:spec 文件一定要进 Git,但生成出来的 pytest 脚本建议也进 Git。原因很简单——CI 上如果生成步骤挂了,你至少还有上一版可运行的脚本兜底,不至于整条流水线红掉。生成是加速手段,不是唯一依赖。