
如果你的测试用例还躺在 Postman 的 Collection 里每天靠手动点按钮去回归我强烈建议你往下看完。这套基于 Pytest Requests Allure Playwright Minium 的接口与 UI 自动化平台是我在维护一个多端项目时逐步搭起来的核心目的只有一个把人肉回归变成命令触发回归。文章会把选型理由、工程结构、关键代码、踩过的坑全部摊开讲适合测试开发、QA 以及想搞自动化但不知道从哪下手的后端同学。后面凡是涉及具体命令和代码的地方我都尽量给全可以直接抄的版本而不是只讲概念。1. 为什么抛弃 Postman不是它不行是场景变了1.1 协作与版本控制的硬伤Postman 本身是个好工具我到现在还在用它做调试、看请求响应、临时验证接口效率确实高。但一旦用例数量上去情况就完全不一样了。我团队里当时有四个测试和两个开发都在维护同一个项目的 Collection经常出现的情况是测试 A 在本地环境把接口地址改成了自己的 mock 地址导出后发到群里别人导入就直接跑到 mock 上去了或者有人在 Collection 的 Pre-request Script 里加了一段取 token 的逻辑自己调试通过了别人一跑就报 401。这类问题的根源不在 Postman 工具本身而在于 Collection 本质上是一个人写好、其他人跟着用的共享文件缺少代码项目里那种分支、合并、评审的规范。改动没有审计版本没有回溯谁改了哪个断言根本查不到更别提把用例接入 CI/CD 流水线了。1.2 断言能力与复杂依赖的短板Postman 的 Tests 标签页支持写 JavaScript 断言pm.expect、pm.response.json() 这些 API 我都用过处理简单的字段校验没问题。但业务接口一旦复杂起来短板就很明显。比如下单接口要求先登录拿 token再调库存查询接口核对库存再判断金额是否符合促销规则最后再下单。这种前置接口 多步数据流转 复杂业务断言的场景在 Postman 里要么靠链式请求和全局变量硬撑要么写出来的脚本逻辑糊成一团维护成本极高。还有一个更现实的问题是接口返回的数据要跟数据库里的订单记录做比对这一步 Postman 里基本做不了你得另写 Python 脚本去连库。结果就是正式的回归验证还是得跑到代码里去完成Postman 充其量只能验证接口通不通。1.3 代码化之后带来什么换到 Pytest 这套体系之后最直观的变化是用例变成了普通 Python 代码能进 Git、能 review、能双击运行、能接流水线。我可以在 fixture 里写登录逻辑让所有用例共享同一份 token可以把测试数据放到 YAML 文件里用例只负责读数据、发请求、断言数据变更不用动代码可以在断言里直接查数据库、比对消息队列里的内容甚至校验缓存。测试代码跟业务代码享受同等的工程规范这才是代码化测试最值钱的地方。它带来的不是自动化率这个虚指标而是每一次跑批之后你能信赖这份结果并且如果有人改动接口用例能第一时间把破坏点指出来。2. 技术选型解析这套组合拳解决什么问题2.1 Pytest从写用例到组织用例的骨架选型时我其实考虑过 unittest也试过 Robot Framework。unittest 是 Python 内置框架零依赖但它写起来啰嗦类继承的方式在用例多了之后层级混乱参数化更是只能靠 subTest 勉强模拟。Robot Framework 的表格语法对不会编程的人友好可一旦涉及复杂断言和自定义处理还得回头写 Python 关键字等于学了两套东西。Pytest 赢在三点fixture 机制做依赖注入和资源管理parametrize 做数据驱动插件生态能覆盖报告、并行、重试、超时等几乎所有需求。pytest-xdist 可以并行执行用例pytest-rerunfailures 可以给不稳定用例加重试pytest-timeout 能防止用例卡死这些都是直接拿来就用的。fixture 是 Pytest 最核心的概念我习惯把它理解成用例的前置条件和清理动作。比如一个模块级别的 fixture 可以做登录并返回 token如果 token 过期就自动重新登录session 级别的 fixture 可以在整个测试会话开始时初始化全局配置。作用域用好了接口用例跑起来非常快因为大部分用例只需要复用同一个登录态而不需要每个用例都重新走一遍登录流程。2.2 Requests轻量但够用的 HTTP 客户端Requests 库本身没什么复杂的就是封装了一套基于 urllib 的 HTTP 客户端get、post、put、delete 一个方法一个动词。我选它的理由也很简单80% 的接口测试场景只需要这个库不需要去上 gRPC、WebSocket 这类重协议框架。它有一个关键特性是 requests.Session()可以自动保存 Cookie还能复用底层 TCP 连接对于需要登录态的接口测试来说省了很多事。Session 对象还可以统一设置 headers、base_url、超时时间这样封装出来的客户端非常干净。项目中如果遇到需要签名、加密的接口直接在封装的 request 方法里做拦截处理加一个 hook 或者子类重写就行不会把逻辑散落在用例代码里。2.3 Allure把测试报告从日志变成证据链我在没有 Allure 之前用的是 pytest-html能出报告也有截图位置但对项目组的价值太有限了。原因是 pytest-html 是一张测试用例结果列表通过率、耗时、日志就这些。Allure 不一样它把每个用例变成了一个可追溯的故事这个用例属于哪个功能模块、测的是哪个业务场景、请求参数是什么、响应是什么、失败时留了什么截图和日志全部结构化展示。对开发来说用例失败后不用跑过来问你这个失败是环境问题还是代码问题打开报告看一眼附件就能定位个大概。Allure 还有历史趋势图、用例执行时长、严重程度分类这些信息在发版决策和用例优化上特别有用。2.4 Playwright MiniumWeb 与小程序的双端覆盖很多人的自动化测试只做了接口层觉得 UI 自动化投入产出比低这个观点我部分同意但有前提纯接口变动不涉及前端交互逻辑接口测够了。可一旦涉及下拉刷新、弹窗、路由跳转、表单联动、支付流程这类前端状态逻辑纯接口测试是看不见问题在哪的。Playwright 解决的是 Web 端 UI 回归它最大的优势是自动等待机制——元素出现且可交互之前操作不会执行这比 Selenium 里手写显式等待省心太多。Minium 则是微信小程序官方的自动化测试框架可以直接驱动小程序运行环境模拟点击、输入、滑动、跳转页面还能读取小程序内部的数据状态。选择这两个工具是想让整个平台从接口通不通延伸到页面踢不踢得动、小程序流程走不走得通覆盖到用户真正操作的路径。3. 接口层落地Pytest Requests 从零搭起3.1 工程目录结构先给出一套我验证过很顺手的目录结构api_test/ ├── config/ │ ├── __init__.py │ ├── settings.py │ └── env.yaml ├── common/ │ ├── __init__.py │ ├── http_client.py │ ├── token_manager.py │ └── assertions.py ├── data/ │ ├── login.yaml │ └── order_cases.yaml ├── testcases/ │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ └── test_order.py ├── reports/ ├── requirements.txt └── pytest.ini目录设计的原则是配置、数据、逻辑、用例、产物五类东西彻底分离。config 放环境地址和公共参数data 放测试数据common 放封装的客户端和断言工具testcases 只放用例本身。这样的好处是一个新人接手项目不看文档也能猜出哪个文件是干嘛的测试数据要改动时也绝不会误伤到用例代码。pytest.ini 里我会配置测试文件命名规则和报告输出目录[pytest] testpaths testcases python_files test_*.py addopts -s -v --alluredirreports/allure-results --clean-alluredir这里 --clean-alluredir 特别重要它会在每次运行前清一次历史 allure-results避免旧结果混进新报告。如果第一次跑完发现 Allure 报告里的用例数是上一次的几倍多半就是没加这个参数。3.2 统一封装 HTTP 客户端requests 直接用问题不大但到处写 requests.post 会让用例代码重复率很高而且一旦要加统一的日志、签名、超时逻辑就得所有用例文件一起改。所以我封装了一个 HttpClient核心代码如下import requests import logging from config.settings import BASE_URL logger logging.getLogger(__name__) class HttpClient: def __init__(self, base_urlBASE_URL, timeout10): self.session requests.Session() self.base_url base_url self.timeout timeout def request(self, method, url, **kwargs): full_url url if url.startswith(http) else self.base_url url kwargs.setdefault(timeout, self.timeout) response self.session.request(method, full_url, **kwargs) logger.info( %s %s, method, full_url) logger.info( body: %s, kwargs.get(json) or kwargs.get(data)) logger.info( status: %s, resp: %s, response.status_code, response.text[:500]) return response def get(self, url, **kwargs): return self.request(GET, url, **kwargs) def post(self, url, **kwargs): return self.request(POST, url, **kwargs)简单解释一下设计所有请求统一走 request()日志记录在封装层完成这样就算后续要接入 Allure 的报告附件也只需要在日志的地方加一句 allure.attach 就够了不用动任何用例代码。为什么保留 session 而不是每次新建 requests.request因为 session 能维持 Cookie也能复用连接池在大量用例并发跑的时候性能和稳定性都有明显差别。3.3 登录态与 Token 管理接口测试绕不开登录态。最常见的方案是在 conftest.py 里定义一个 session 级别的 fixture先调登录接口拿到 token然后把 token 放进 HttpSession 的 headers 里后续用例直接复用。我用的 token 管理是放在 common/token_manager.pyimport json import time from common.http_client import HttpClient _token_cache {value: None, expire_at: 0} def get_token(username, password, forceFalse): if not force and _token_cache[value]: return _token_cache[value] client HttpClient() resp client.post(/api/v1/login, json{username: username, password: password}) data resp.json() _token_cache[value] data[access_token] _token_cache[expire_at] data[expires] time.time() return _token_cache[value]token 缓存逻辑里有个细节如果只缓存 token 而不记录过期时间跑一小时后用例就会因为 token 过期全部失败而且失败信息全是 401根本看不出是业务挂了还是登录态过期了。所以我在缓存里加了一个 expire_atfixture 在取 token 的时候判断一下是否接近过期快过期就强制刷新。这样既避免每个用例独立登录的耗时又不会踩到 token 过期导致的批量失败。conftest.py 里的 fixture 写法import pytest from common.http_client import HttpClient pytest.fixture(scopesession) def client(): c HttpClient() token get_token(tester, 123456) c.session.headers.update({Authorization: fBearer {token}}) yield c注意这里的 scope 是 session意味着整个测试会话只用这一个客户端登录只做一次。但如果跑批时间非常长token 在会话中途过期就需要在 fixture 里加一个响应 401 后自动重新登录并重放请求的逻辑这个我后面在常见问题里展开。3.4 数据驱动与参数化接口测试里最烦的不是写用例而是同一套流程要覆盖 N 组数据。比如创建订单接口需要覆盖正常下单、余额不足、商品下架、库存为 0、参数缺失这些场景如果每种场景写一个 test 函数代码重复率会高到让人崩溃。Pytest 的 parametrize 就是为了解决这个问题的import pytest import yaml with open(data/order_cases.yaml, encodingutf-8) as f: cases yaml.safe_load(f) pytest.mark.parametrize(case, cases, ids[c[title] for c in cases]) def test_create_order(client, case): resp client.post(/api/v1/orders, jsoncase[request]) assert resp.status_code case[expected][status] assert resp.json()[code] case[expected][code]parametrize 的 ids 参数强烈建议加上否则报告里每一行用例都长得一样根本分不清哪条是余额不足哪条是商品下架。测试数据放 YAML 的原因也很简单业务同学可以直接维护数据文件不需要打开代码而且 YAML 支持注释能写清楚每条用例对应的业务场景。如果要切换数据源比如从线上订单池拉数据、从数据库查出的动态数据生成用例parametrize 照样支持因为 cases 可以是任何 Python 列表。3.5 断言别只停留在状态码很多入门教程的断言就写到 assert resp.status_code 200但真实项目里这远远不够。200 只是HTTP 层没挂并不代表业务成功。我见过的最典型例子是接口返回 200业务 code 是 50001库存不足前端弹了个报错而往上的自动化脚本还在显示通过。所以我在断言上分了三个层次第一层是 HTTP 状态码保证服务可用第二层是业务返回码保证业务逻辑符合预期第三层是数据校验包括关键字段值、数据库记录、甚至调第三方接口返回的数据。第三层在代码里体现为用例里发完请求后再连数据库查一下订单状态是不是预期的或者调一个查询接口核对落库数据。这样断言的不再是接口返回了什么而是系统实际做成了什么。如果你团队里有测试开发的同事可以让他把数据库查询封装成 db_query 工具用例里一行调用即可成本不高收益非常明显。4. Allure 报告集成让每次跑批都有据可查4.1 环境安装与命令行Allure 分为两部分pytest 侧的插件 allure-pytest 负责在测试执行时收集结果数据就是那个 allure-results 目录以及报告生成器 allure 命令行工具负责把结果渲染成 HTML 页面。pip install allure-pytest命令行工具在不同系统上安装方式不同macOS 上 brew install allureWindows 上可以用 scoop install allure也可以直接下载官方压缩包把 bin 目录加到 PATH。安装完敲 allure --version 确认一下。生成报告的完整流程是pytest --alluredirreports/allure-results --clean-alluredir allure generate reports/allure-results -o reports/allure-report --clean allure open reports/allure-report我在实际项目里一般还会把 allure generate 写进 CI 流水线脚本跑完用例自动出报告并归档。注意 generate 里的 --clean 是清理旧的 HTML 报告目录不是清理 results两者别搞混。4.2 用例层级与描述信息Allure 报告的信息密度取决于你在用例上贴了多少标签。我常用的装饰器是 feature、story、title、description层级上 feature 对应大的功能模块story 对应具体的业务场景title 是给人看的中文描述。示例import allure allure.feature(订单模块) allure.story(创建订单) allure.title(余额不足时创建订单应提示失败) allure.description(验证余额不足场景下的业务拦截逻辑) def test_create_order_insufficient_balance(client): ...这样配完之后报告首页会按照 feature 和 story 自动分组点进去每个用例都有标题和描述这是 pytest-html 做不到的。另外 allure.step 装饰器可以把用例里的关键步骤拆成可折叠的子步骤比如准备测试数据调用下单接口校验数据库记录失败时能精确到哪一步出问题。4.3 请求日志与响应快照用例失败后最有价值的调试信息是请求到底发出了什么、服务端回了什么。如果你不想让用例代码里到处写 print可以在封装的 HttpClient 里加一个通用钩子把每次请求的 method、url、headers、request body、response status、response body 统统 attach 到 Allure 报告import allure def attach_to_allure(response, *args, **kwargs): allure.attach( f{response.request.method} {response.request.url}, nameRequest URL, attachment_typeallure.attachment_type.TEXT, ) allure.attach( response.request.body or , nameRequest Body, attachment_typeallure.attachment_type.JSON, ) allure.attach( response.text, nameResponse Body, attachment_typeallure.attachment_type.JSON, ) return response然后在 HttpClient.request 的最后调用 attach_to_allure整个平台的调试成本瞬间降下来。开发同学在收到报告链接后基本都能自己判断是入参问题还是服务端逻辑问题不需要再来回拉扯。4.4 历史趋势与 CI 环境信息Allure 有历史趋势图但有个坑它默认只展示当次报告的数据历史趋势需要把上一次报告的 history 数据复制到新报告里。做法很简单在生成报告前保留旧报告根目录的 history 文件夹generate 的时候把它拷回去if [ -d reports/allure-report/history ]; then cp -r reports/allure-report/history reports/allure-results/ fi allure generate reports/allure-results -o reports/allure-report --clean另外为了让报告展示环境信息比如被测环境地址、浏览器版本、JDK 版本、执行机器可以在 allure-results 目录下放一个 environment.properties 文件base.urlhttps://test-api.example.com browserchromium pytest.version8.0.0如果你用的 CI 是 Jenkins还可以放 executor.json 文件报告里就会显示本次执行由 jenkins 的哪个 job 触发、谁触发的、构建号是多少排查问题的效率会好很多。5. UI 自动化Playwright 把 Web 回归接进来5.1 安装与基础用法Playwright 的安装分两步pip 装 pytest-playwright然后用 playwright install 下载浏览器内核。注意它默认会下载 Chromium、Firefox、WebKit 三个内核如果公司网络受限可以只下 Chromium命令是 playwright install chromium。pytest-playwright 插件会提供 page、browser、context 这些现成的 fixture你不需要自己管理浏览器实例直接写用例就行def test_search_menu(page): page.goto(https://test.example.com) page.get_by_role(button, name搜索).click() expect(page.locator(.result)).to_be_visible()跟 Selenium 最大的区别是 Playwright 有自动等待click、fill 这些操作会先等待元素可交互不需要你写 sleep 或者显式 WebDriverWait。早期从 Selenium 转过来的人最容易犯的错就是还带着写 time.sleep 的习惯把用例速度拖得很慢其实在 Playwright 里绝大多数情况都不需要它。5.2 Page Object 模式落地UI 用例如果直接在测试函数里写一堆 page.locator 调用页面一改版所有用例文件都得跟着改这就是 UI 自动化维护成本爆炸的最大来源。Page Object 模式POM的思路是一个页面一个类类里只放页面元素的定位和操作方法用例层只描述业务场景。一个最小可用的例子class LoginPage: def __init__(self, page): self.page page self.username_input page.get_by_placeholder(请输入用户名) self.password_input page.get_by_placeholder(请输入密码) self.submit_button page.get_by_role(button, name登录) def goto(self): self.page.goto(https://test.example.com/login) def login(self, username, password): self.username_input.fill(username) self.password_input.fill(password) self.submit_button.click() def test_login_success(page): login_page LoginPage(page) login_page.goto() login_page.login(tester, 123456) expect(page.get_by_text(欢迎回来)).to_be_visible()POM 的核心价值是变更收敛页面元素变了只改 Page 类业务流程变了只改用例方法。配合 Playwright 的 codegen 工具直接录制定位器写 Page 类的效率也还不错。5.3 等待策略与稳定性调优UI 自动化最大的敌人是不稳定脚本报错了手动去看页面又没问题最后只能重跑碰运气。Playwright 自动等待能解决一部分但不是万能的。最典型的是接口返回慢的场景点击按钮后页面要等接口数据渲染Playwright 的自动等待只覆盖了 DOM 元素覆盖不了数据还没回来的半成品状态。我的经验是优先用 to_be_visible、to_have_text 这类 expect 断言替代固定 sleep需要等接口响应的场景用 page.expect_response 等待特定接口返回比写死等待靠谱需要整体调整等待时间时在 fixture 里设置 context 的 timeout而不是在用例里到处加 sleep。下面这一段是我常用的 fixture 写法它会在用例失败时自动截图并附到 Allure 报告里实现失败即留证据pytest.fixture def page(browser): ctx browser.new_context(viewport{width: 1280, height: 720}) page ctx.new_page() yield page if page.screenshot(): allure.attach(page.screenshot(), namefailure_screenshot, attachment_typeallure.attachment_type.PNG) ctx.close()5.4 限流与重试碰到 429 别慌UI 回归经常是一口气跑上百个用例访问频率一高就会把服务端的限流策略给触发最常见的表现就是接口返回 429 Too Many Requests。很多人第一次看到这个状态码会一脸懵误以为是脚本写错了其实它只说明一个问题你没被拦在门外而是被限了速。处理 429 有几种思路按优先级排列降低并发pytest-xdist 的并发数调低或者改为串行跑关键链路重试 退避用 tenacity 库对请求加重试指数退避比如第一次等 1 秒第二次等 2 秒第三次等 4 秒请求频率限速批量数据类用例之间加轻量 sleep不要一毫秒都不隔地连着打与后端沟通如果是批量触发建议申请独立的测试账号或者让后端在测试环境把限流阈值调高。下面是一个简单的重试示例from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type class TooManyRequestsError(Exception): pass retry( reraiseTrue, stopstop_after_attempt(3), waitwait_exponential(multiplier1, min1, max8), retryretry_if_exception_type(TooManyRequestsError), ) def call_api(client, **kwargs): resp client.post(...) if resp.status_code 429: raise TooManyRequestsError(rate limited) return resp这里我用的是 tenacity 库而不是自己写 while 循环原因很简单retry 配置可以通过装饰器参数一眼看清重试策略代码也干净。注意别把 429 重试逻辑加到所有请求上只加在批量触发的关键链路上否则限流没触发重试反而拖慢整个测试。另外提醒一句如果你的被测系统带了验证码、滑块这类风控策略不要试图在脚本里绕过它正确做法是让后端在测试环境临时关闭风控或配置白名单账号测试代码永远不要跟线上的安全机制较劲。6. 小程序测试Minium 补上移动端拼图6.1 为什么选 Minium微信小程序的 UI 自动化有 Appium、Airtest、Minium 几个选择。Appium 是通用移动端框架但对小程序的兼容性依赖 WebView 调试开关环境配置比较折腾Airtest 图像识别在元素动态变化的场景下不够稳定而且灰度匹配那套处理不当很容易误判。Minium 是微信官方推出的测试框架最大的优势是它跟小程序运行时是同架构的可以直接获取页面元素、调用小程序内部 API、读取页面 data 数据不需要通过图像识别或者 WebView 的曲折路径。如果你的项目正好有小程序端Minium 基本是绕不开的选择。6.2 准备工作与自动化通道Minium 的使用前提是先安装微信开发者工具并且在开发者工具里开启自动化测试的服务端口。这个端口不打开Minium 连不上工具报错通常是 connect fail 之类。开启路径在微信开发者工具的设置 - 安全设置里找到服务端口开关打开后工具会监听一个本地端口Minium 默认使用 9420 端口如果你的工具用了别的端口要在配置里指定。安装和配置pip install miniumMinium 需要一个配置文件指定小程序项目的绝对路径、开发者工具的安装路径、目标环境等一个典型配置如下{ project_path: D:/work/miniprogram, dev_tool_path: C:/Program Files/微信web开发者工具/cli.bat, debug_mode: off, enable_sat: true }注意 project_path 必须指向小程序项目根目录不是 dist 目录dev_tool_path 在不同操作系统上路径差异很大Windows 上是 cli.batmacOS 上是 cli。配完之后可以跑一个最简单的 smoke 用例验证连接通了再往下写。6.3 第一个小程序用例Minium 的用例写法跟标准 Pytest 用例差不多核心是 MiniTest 这个类实例化的 mini 对象它管理着小程序的整个生命周期import minium class FirstTest(minium.MiniTest): def test_open_page(self): self.page.get_element(view, inner_text首页).click() self.page.get_element(view, inner_text我的).click() self.assertEqual(self.page.path, pages/mine/index)这只是最简单的验证。实际项目里更常见的是验证小程序内部的表单校验、列表数据渲染、跳转逻辑。Minium 还支持直接给页面传参、调用项目里的方法、读取数据绑定这比在浏览器里操作要深入得多因为你能看到小程序真实的内部状态。6.4 API 与 UI 的全链路串联只把 Web 和接口分开测还谈不上平台。我的做法是把测试流程串起来先用接口层准备数据再用 UI 层做端到端验证最后回到接口层做结果核对。一个典型场景是下单-支付-订单列表全流程接口层调用创建订单接口拿到订单号接口层调用支付接口把订单状态推到已支付UI 层在小程序或 Web 页面里进入订单列表验证已支付订单出现在列表最前面接口层调订单详情接口断言数据库状态与 UI 显示一致。这里最关键的编排逻辑在 Pytest 层完成。你可以把接口步骤写成 helper 函数UI 步骤写成 Page Object 方法然后在同一个 test 函数里按顺序调用。这样既不像纯接口测试那样对前端交互一无所知也不像纯 UI 测试那样每个场景都得从零开始点击速度和稳定性都有保障。我建议所有涉及支付、数据准备的高成本场景都用这种接口铺路 UI 验证的模式。7. 常见问题与排查技巧实录7.1 高频问题速查表现象可能原因解决方法pytest 收集不到用例文件名或目录不符合规则确认文件名是 test_*.py或检查 pytest.ini 的 testpaths/python_files接口请求失败但 Postman 能通缺少必要请求头或环境地址不对对比 Postman 的请求头检查 base_url 是否指向正确环境Allure 报告没有数据allure-results 没有生成或被污染加 --clean-alluredir确认 allure-pytest 已安装Allure 命令行报错allure 工具未安装或 PATH 未配安装命令行工具并确认 allure --version 能执行Playwright 定位超时元素在 iframe 里、或页面用了动态渲染用 frame_locator 定位 iframe检查期望等待条件是否合理429 Too Many Requests触发了服务端限流降低并发、加重试退避、协调测试环境限流阈值Minium 连不上开发者工具服务端口未开启或路径配置错在开发者工具安全设置开启服务端口并检查配置路径用例执行慢每个用例都重新登录使用 session 级别 fixture 复用登录态7.2 我踩过的几个坑和补救方案第一个坑是 token 并发问题。一开始我把登录 token 直接写在 fixture 里用 pytest-xdist 跑并行的时候每个 worker 进程各自调一次登录接口因为同一账号不可并发登录结果后登录的 worker 把前面登录的 token 全踢下线了用例大面积 401。后来我把 token 管理改成了文件缓存 互斥锁或者干脆给每个 worker 分配独立的测试账号问题才算解决。如果你也打算用 xdist 并行账号设计一定要考虑到并发隔离。第二个坑是 Allure 历史趋势怎么都不出来。原因是每次 generate 报告时把 history 目录也 clean 掉了趋势图只有单独的孤点。后来我在脚本里加了一步从上一份报告里单独备份 history 目录测试结束后先拷贝回 allure-results 再 generate趋势图就正常了。第三个坑是数据库断言带来的环境耦合。为了做第三层断言我在用例里加了数据库查询本地环境跑得很好到了测试环境的 CI 上却全部失败——原因是 CI 执行机的 IP 不在数据库白名单里。这个问题的教训是数据库断言必须做成可配置开关并且在 CI 环境里先跟 DBA 沟通放行白名单而不是写死在用例里。最后一个是重试导致的假通过。我有段时间给所有用例都挂了 pytest-rerunfailures重试 2 次结果有很多用例第一次就失败第三遍才通过报告里虽然显示绿色但实际上是碰运气而不是稳定通过。后来我做了个约定重试只允许用于环境波动导致的连接类错误业务断言失败一律不重试并且报告里要能看出来哪些用例是经过重试才通过的。否则自动化不仅没降低成本反而制造了一种虚假的安全感。说实话这套平台搭完之后并没有出现什么自动化解放人力的神话。回归用例从上百条变成跑一次十分钟该人工确认的地方还是要人工。但最大的变化是每次发版前我能很确定地说接口层面没有回归问题Web 和小程序的关键链路是通的哪些用例是 flaky 需要重点跟进哪些接口真的出现了数据不一致。这种可计算、可追溯的安全感是 Postman 点来点去给不了的。如果你也在从纯手动走向代码化测试我的建议是从接口层先起步跑通了再加 UI 和小程序一步到位往往容易翻车。