做测开这几年,我最大的感受就是:真正耗时间的不是业务测试逻辑本身,而是每接一个新项目,都要从零搭一遍自动化框架。目录结构重新建、公共方法重新写、日志和报告重新配,甚至连“怎么读取配置”这种几十行代码都要再敲一遍。这种重复劳动,干一次是学习,干十次就是纯纯的内耗。所以我花了不少精力,把日常工作里最高频的两套自动化场景沉淀成了通用模板:一套是面向接口自动化的 pytest + requests 框架,另一套是面向 UI 自动化的 pytest + Selenium/Playwright 框架。两套模板的核心思路都一样:分层清晰、开箱即用、替换业务代码就能跑。这篇文章就把这两套模板的结构、关键实现和落地细节完整拆开,希望能给正在搭框架或准备重构的测开同学一些参考。1. 为什么测开需要两套通用模板1.1 重复造轮子的代价,比想象中更大很多团队的第一套自动化脚本都是“小作坊”模式:一个脚本文件从头写到尾,公共函数塞在 utils 里,测试数据写在代码里。这种写法在用例只有十几条的时候还挺爽,因为改起来直接,跑起来也快。但用例一旦超过 50 条,或者需求迭代进入第二个月,问题就全出来了。用例之间开始互相依赖,某个接口的响应被另一个用例硬编码引用,前端元素定位散落在各个脚本里,一旦页面改版,排查都要排查半天。我见过比较夸张的一个项目,自动化用例 80% 都是拷贝粘贴改出来的,公共方法有五个版本,谁也不敢动。后来实在跑不下去了,只能花两个迭代重构。与其这样,不如在最开始就准备一套通用模板,把目录结构、公共方法、数据驱动机制、报告输出全部固定下来,业务代码只是往里面填。1.2 两套模板的适用边界和选型逻辑为什么偏偏是这两套,而不是“一套模板走天下”?因为测试金字塔的规律摆在那里:接口自动化成本低、稳定性高、执行快,适合覆盖业务逻辑和异常链路;UI 自动化贴近用户真实操作,适合覆盖关键主流程和端到端验证,但成本高、容易受环境波动影响。两者互补,但不能互相替代。我的建议是:如果被测对象有明确接口,优先用接口自动化模板覆盖绝大部分回归场景;如果涉及多系统串联、前端交互验证、或者需要模拟真实用户操作,再引入 UI 自动化模板;如果业务还在快速迭代、页面结构一天一变,UI 自动化可以先只做主流程,别急着全覆盖。这个选型逻辑说白了就是 ROI。同样写一条用例,接口用例可能十几分钟搞定,UI 用例可能要半天,还得持续维护。用模板不是为了炫技,而是为了把固定成本降下来,让自动化在项目里能真正活下去。2. 第一套模板:接口自动化通用框架2.1 目录结构:一眼就能看懂的分层接口自动化模板我建议按照“数据驱动 + 分层”的思路来组织,目录结构大概是这样的:api_test_template/ ├── config/ │ ├── __init__.py │ ├── settings.py # 全局配置、环境切换 │ └── dev.yaml # 开发环境配置 ├── core/ │ ├── __init__.py │ ├── http_client.py # requests 会话封装 │ ├── assert_utils.py # 断言工具 │ ├── logger.py # 日志封装 │ └── read_data.py # yaml/excel/json 读取 ├── data/ │ ├── login_data.yaml # 登录接口测试数据 │ └── order_data.yaml # 订单接口测试数据 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # fixture 和钩子 │ ├── test_login.py │ └── test_order.py ├── reports/ │ └── ... # 测试报告输出目录 ├── requirements.txt ├── pytest.ini └── run.py # 统一入口脚本这套分层的逻辑很简单:config 管环境,core 管能力,data 管数据,testcases 管业务。测试用例里基本看不到 requests 的细节,也不会有环境地址写死的情况,看到的都是业务描述和数据引用。优势很明显:新人进来不用看全部代码,只需要知道“我要测什么接口,应该往 data 里加数据、往 testcases 里加用例”。2.2 核心模块的实现与理由先说 http_client.py,这是所有接口调用的底座。我封装它的原因有三个:统一处理基础请求头、统一异常捕获、统一日志记录。如果没有这一层,每个用例里直接调 requests.post,且不说代码重复,出了问题连最基本的请求耗时、返回状态码都没人记。# core/http_client.py import requests import time from core.logger import get_logger logger = get_logger(__name__) class HttpClient: def __init__(self, base_url, timeout=10): self.session = requests.Session() self.base_url = base_url self.timeout = timeout self.session.headers.update({ "Content-Type": "application/json;charset=UTF-8" }) def request(self, method, path, **kwargs): url = self.base_url + path kwargs.setdefault("timeout", self.timeout) start_ts = time.time() try: resp = self.session.request(method, url, **kwargs) logger.info(f"请求 {method} {url} 状态码={resp.status_code} 耗时={time.time() - start_ts:.3f}s") return resp except requests.RequestException as exc: logger.error(f"请求异常 {method} {url}: {exc}") raise def get(self, path, **kwargs): return self.request("GET", path, **kwargs) def post(self, path, **kwargs): return self.request("POST", path, **kwargs)注意这里我用的是 requests.Session 而不是裸调 requests.get/post。Session 会自动管理连接池,在跑大量用例的时候能复用底层 TCP 连接,实测同一套用例用 Session 比裸请求整体能快 20% 到 30%,这个提升在回放 200 条用例时体感非常明显。conftest.py 是 pytest 的灵魂。我在里面定义了一个全局的 client fixture,以及一个自动读取 yaml 测试数据的钩子:# testcases/conftest.py import pytest import yaml from core.http_client import HttpClient from config.settings import BASE_URL @pytest.fixture(scope="session") def client(): http_client = HttpClient(BASE_URL) yield http_client http_client.session.close() def load_test_data(file_name): with open(f"data/{file_name}", encoding="utf-8") as f: return yaml.safe_load(f) @pytest.fixture() def login_data(): return load_test_data("login_data.yaml")用 yaml 做数据驱动是我比较坚持的做法。对比 Excel 和 JSON,yaml 的书写成本低,支持注释,层级关系清晰,而且天然就是 Python 字典的结构。Excel 适合非技术人员维护大量数据,JSON 写起来括号多容易错,yaml 在这三者里是对测开同学最友好的一种。2.3 数据驱动和多环境切换的提效点数据驱动的核心不是“把数据放到 yaml 里”这个动作,而是“用例逻辑与测试数据分离”这种思维。举一个例子:登录接口需要验证正常登录、密码错误、账号不存在、账号锁定等多种场景。如果不用数据驱动,每条用例都是一段几乎一样的代码。用数据驱动之后,用例部分可以收敛成一条:# testcases/test_login.py import pytest class TestLogin: @pytest.mark.parametrize("case", [ {"desc": "正常登录", "payload": {"username": "admin", "password": "123456"}, "code": 0}, {"desc": "密码错误", "payload": {"username": "admin", "password": "wrong"}, "code": 1001}, {"desc": "账号不存在", "payload": {"username": "nobody", "password": "123456"}, "code": 1002}, ]) def test_login(self, client, case): resp = client.post("/api/login", json=case["payload"]) assert resp.json()["code"] == case["code"]这样写有一个明显的好处:以后新增场景只需要往列表里加一个字典,不用新增方法,也不会影响已有用例。而且用例名称、描述、预期结果全部一目了然,测试报告读起来就是业务语言,而不是一堆看不懂的 response body。多环境切换我放在了 settings.py 里解决。做法很简单:读取环境变量中的 RUN_ENV,默认走 dev,通过环境变量切到 staging 或 prod 配置,而不是每次改代码。我之前见过有人在代码里直接改 BASE_URL,然后不小心把带数据的测试环境给刷了,这种事故就是配置管理不规范导致的。用模板之后就再没出现过这种问题。3. 第二套模板:UI 自动化与端到端用例框架3.1 UI 自动化为什么更需要模板UI 自动化比接口自动化更容易失控,原因是它天然依赖环境:浏览器版本、驱动程序、页面加载速度、网络波动、弹窗遮挡,任何一个环节抖动,用例就可能挂。如果没有一套统一模板,每个用例自己处理等待、自己截图、自己拼元素定位,那最终结果就是“今天能跑,明天不能跑,没人知道为什么”。所以 UI 自动化模板的第一个目标不是写更多用例,而是建立稳定性机制。第二个目标才是复用。页面对象(Page Object Model,简称 POM)模式就是为此设计的:把页面元素定位和操作方法封装成类,测试用例只描述业务动作,不碰 CSS 选择器或 XPath。3.2 POM 模式与公共操作封装我在模板里用的是 pytest + Selenium,同时对关键公共操作进行了二次封装。为什么不用原生 find_element?因为原生方法在元素不存在时直接抛异常,不会等待,也不会留下现场信息,对于排查问题来说很不友好。封装后的方法长这样:# core/page_base.py import time from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class PageBase: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def click(self, locator): elem = self.wait.until(EC.element_to_be_clickable(locator)) elem.click() def fill(self, locator, text): elem = self.wait.until(EC.visibility_of_element_located(locator)) elem.clear() elem.send_keys(text) def get_text(self, locator): elem = self.wait.until(EC.visibility_of_element_located(locator)) return elem.text def screenshot(self, name): self.driver.save_screenshot(f"reports/screenshots/{name}_{int(time.time())}.png")这里的关键点是“显式等待”,而不是 time.sleep 这种固定等待。显式等待是条件满足就继续,不满足就等到超时;固定等待不管页面状态如何,都要傻等几秒,既慢又不稳定。实测下来,同样一版用例,把 sleep 全部换成显式等待之后,执行时间能缩短三分之一左右,稳定性还有明显提升。页面对象类则对应业务页面,比如登录页、订单页:# pages/login_page.py from selenium.webdriver.common.by import By from core.page_base import PageBase class LoginPage(PageBase): username_input = (By.CSS_SELECTOR, "#username") password_input = (By.CSS_SELECTOR, "#password") login_button = (By.CSS_SELECTOR, "button[type='submit']") def login(self, username, password): self.fill(self.username_input, username) self.fill(self.password_input, password) self.click(self.login_button)这样设计之后,用例层就变得非常薄:# testcases/test_login_page.py from pages.login_page import LoginPage class TestLoginPage: def test_user_login(self, driver): page = LoginPage(driver) page.open("/login") page.login("admin", "123456") assert page.get_current_url().endswith("/home")页面元素一旦变化,只需要改页面对应类里的一行定位,而不是满项目找在哪里用到了这个输入框。这就是 POM 的最大价值:把变化的范围限制在一个地方。3.3 稳定性治理:等待、截图、重试、失败处理UI 自动化最怕“偶现失败”。同一段用例,跑三遍挂一遍,还复现不了。我的经验是:遇到偶现失败,不要急着改业务逻辑,先看失败时的截图和录屏,八成是页面加载慢或者元素被弹出框遮挡。模板里内置了几个稳定性组件:第一个是失败自动截图。我在 pytest 的 hook 里加了失败处理,用例失败时自动保存截图并附加到 HTML 报告,这样一眼就能看出是定位错了还是页面崩了。第二个是失败重试机制。用了 pytest-rerunfailures 插件,对已知的不稳定用例设置 reruns=2,只对 UI 层用例开,接口层不建议开,因为接口层失败往往是真实逻辑问题,重试反而掩盖 bug。第三个是 headless 模式。CI 环境没有界面,运行到浏览器时如果没配置 headless,直接报错。模板里用一个配置开关控制,本地调试时开界面模式,CI 里切 headless。关于 Selenium 和 Playwright 怎么选,我个人的倾向是:如果团队已经熟悉 Selenium,生态成熟,没必要迁移;如果是新团队从零开始,直接上 Playwright 会更省事。Playwright 自带浏览器驱动管理、自动等待、录制、多标签页处理都更顺手。但这不代表 Selenium 过时了,很多老项目和开源生态都还在用 Selenium,关键是模板机制本身是通用的,底层引擎可以替换。4. 配套的日常提效工具箱4.1 日志和报告:不能只在本地跑通就没后续自动化框架跑完出一份报告,这算是基本要求,但很多模板会忽略日志的“可检索性”。我见过一些项目的 log 文件不分级别、不分时间,全塞在一个文件里,出了问题 Ctrl+F 找关键词找半天。模板里我用 logging 模块按天生成日志,并且把请求状态、响应摘要、耗时都打进去。这样即使测试报告没细看,也能通过日志快速定位到具体是哪个请求导致的失败。报告这块,接口层我用 pytest-html 加自定义摘要,UI 层用 pytest-html 附带截图展示。没有上太重型的测试平台,因为对多数项目来说,一个静态 HTML 报告加上错误日志,足够日常回归使用了。真要上平台,模板的用例结构也可以无缝对接,因为数据驱动和分层设计与 Allure 这类工具天然兼容。4.2 一条命令完成环境初始化模板要做到“拿来就能跑”,环境初始化就不能靠手点。我在根目录放了一个 setup 脚本,做的事情包括:创建虚拟环境、安装依赖、检查浏览器驱动、初始化报告目录。首次使用只需要执行:python setup.py pytest -v这里有一个细节:依赖版本不要全部放开,最好锁定大版本,尤其像 requests、pytest 这些核心包。我在 requirements.txt 里写的是:pytest>=7.0,<8.0 requests>=2.28,<3.0 PyYAML>=6.0,<7.0 pytest-html>=3.2,<4.0锁定大版本是为了避免自动化跑着跑着,因为某个库发布了不兼容的新版,整个框架突然崩掉。测开框架的第一要务是可控,不是追新。4.3 从模板到业务的 5 步落地流程模板本身不是最终交付物,业务用例才是。我复制模板到新项目时,通常会走一套固定的流程:复制模板目录,改项目名和包名;在 config 里新增被测环境的 base_url、账号、密钥等基础配置;根据接口文档或页面清单,在 data 目录或 pages 目录下补业务数据/页面对象;写第一批核心用例,跑通一条完整链路;提交代码,配置定时任务或 CI 触发。这五步走下来,一个项目的第一版自动化框架通常半天就能跑起来,剩下的时间全部投入业务用例编写。这才是模板真正“提效”的地方:它把最没价值的搭架子环节压缩到最小。5. 常见问题与排查技巧实录5.1 高频问题速查表下面这张表是我在实际推广模板过程中被问得最多的问题,连同排查思路一起列出来:现象可能原因排查方法与处理接口用例能跑到一半突然超时接口依赖的第三方服务不稳定查看日志里超时接口的耗时,确认是网络问题还是服务问题,对非核心依赖加 try 或延迟重试切换环境后大量用例失败配置没切换干净或测试数据污染检查所有 base_url 引用是否都走了配置,确认环境数据是否被上一次测试写脏UI 用例偶发找不到元素元素是异步加载的把固定 sleep 替换为显式等待,确认元素是否在 iframe/Shadow DOM 中浏览器启动后 driver 报版本不匹配浏览器自动升级,driver 版本落后模板支持自动下载匹配驱动,或固定浏览器版本报告里截图看不到报告目录被清理或路径含中文字符统一用绝对路径拼接报告目录,避免相对路径在不同目录下跑乱套数据驱动用例太多,单条定位困难用例 ID 不清晰给每条用例增加业务化 name,测试报告中用 label 标注模块和场景5.2 几个值得长期坚持的避坑习惯第一个习惯是“用例之间永远不要有执行顺序依赖”。我见过不少团队的用例是 A 登录、B 下单、C 支付,C 依赖 B 的订单号,B 依赖 A 的 token,一旦单独跑 C 就挂。这种用例集连基本的独立性都没有,根本算不上自动化回归。正确做法是每个用例自己完成前置数据准备,要么通过接口造数,要么在用例内部调用公共方法创建依赖对象。第二个习惯是“不要试图让 UI 用例覆盖所有异常场景”。异常分支和边界值优先用接口自动化覆盖,UI 层只保留用户最典型的高频操作路径。这样既能控制执行时间,也能减少维护成本。我见过团队一门心思把 UI 用例数量做到上千条,结果每天光是修失败用例就忙到下班,这就本末倒置了。第三个习惯是“模板要持续迭代,不能一沉不变”。比如之前我的接口模板用 requests,后来团队引入 gRPC,我就把 core 层扩展了一下,新增了一个 grpc_client。模板是活的东西,随着团队技术栈变化要持续维护,否则它的价值会随着时间慢慢衰减。6. 一点个人体会如果只挑一条经验分享给刚接触自动化的测开同学,我可能会说:先搭框架,再谈用例数量。框架决定了你的自动化能稳定跑多久,用例数量决定了你的自动化能覆盖多少业务。没有框架的用例是负债,有了框架的用例才是资产。这两套模板花了我不少业余时间打磨,但带来的回报也确实可观。现在不管接手什么新项目,我基本不用考虑“自动化架子怎么搭”这个问题,直接套模板,把精力集中在理解业务、设计用例上。这可能就是“提效”的本质——不是让你在同一件事上做得更快,而是让你把时间分配到更有价值的事情上去。后续如果有空,我可能会把两套模板整合到一个 CLI 工具里,做成“一条命令生成指定类型的自动化项目”,再结合任务调度做自动化巡检。等玩出效果了,再回来继续分享。
做测开这几年,我最大的感受就是:真正耗时间的不是业务测试逻辑本身,而是每接一个新项目,都要从零搭一遍自动化框架。目录结构重新建、公共方法重新写、日志和报告重新配,甚至连“怎么读取配置”这种几十行代码都要再敲一遍。这种重复劳动,干一次是学习,干十次就是纯纯的内耗。所以我花了不少精力,把日常工作里最高频的两套自动化场景沉淀成了通用模板:一套是面向接口自动化的 pytest + requests 框架,另一套是面向 UI 自动化的 pytest + Selenium/Playwright 框架。两套模板的核心思路都一样:分层清晰、开箱即用、替换业务代码就能跑。这篇文章就把这两套模板的结构、关键实现和落地细节完整拆开,希望能给正在搭框架或准备重构的测开同学一些参考。1. 为什么测开需要两套通用模板1.1 重复造轮子的代价,比想象中更大很多团队的第一套自动化脚本都是“小作坊”模式:一个脚本文件从头写到尾,公共函数塞在 utils 里,测试数据写在代码里。这种写法在用例只有十几条的时候还挺爽,因为改起来直接,跑起来也快。但用例一旦超过 50 条,或者需求迭代进入第二个月,问题就全出来了。用例之间开始互相依赖,某个接口的响应被另一个用例硬编码引用,前端元素定位散落在各个脚本里,一旦页面改版,排查都要排查半天。我见过比较夸张的一个项目,自动化用例 80% 都是拷贝粘贴改出来的,公共方法有五个版本,谁也不敢动。后来实在跑不下去了,只能花两个迭代重构。与其这样,不如在最开始就准备一套通用模板,把目录结构、公共方法、数据驱动机制、报告输出全部固定下来,业务代码只是往里面填。1.2 两套模板的适用边界和选型逻辑为什么偏偏是这两套,而不是“一套模板走天下”?因为测试金字塔的规律摆在那里:接口自动化成本低、稳定性高、执行快,适合覆盖业务逻辑和异常链路;UI 自动化贴近用户真实操作,适合覆盖关键主流程和端到端验证,但成本高、容易受环境波动影响。两者互补,但不能互相替代。我的建议是:如果被测对象有明确接口,优先用接口自动化模板覆盖绝大部分回归场景;如果涉及多系统串联、前端交互验证、或者需要模拟真实用户操作,再引入 UI 自动化模板;如果业务还在快速迭代、页面结构一天一变,UI 自动化可以先只做主流程,别急着全覆盖。这个选型逻辑说白了就是 ROI。同样写一条用例,接口用例可能十几分钟搞定,UI 用例可能要半天,还得持续维护。用模板不是为了炫技,而是为了把固定成本降下来,让自动化在项目里能真正活下去。2. 第一套模板:接口自动化通用框架2.1 目录结构:一眼就能看懂的分层接口自动化模板我建议按照“数据驱动 + 分层”的思路来组织,目录结构大概是这样的:api_test_template/ ├── config/ │ ├── __init__.py │ ├── settings.py # 全局配置、环境切换 │ └── dev.yaml # 开发环境配置 ├── core/ │ ├── __init__.py │ ├── http_client.py # requests 会话封装 │ ├── assert_utils.py # 断言工具 │ ├── logger.py # 日志封装 │ └── read_data.py # yaml/excel/json 读取 ├── data/ │ ├── login_data.yaml # 登录接口测试数据 │ └── order_data.yaml # 订单接口测试数据 ├── testcases/ │ ├── __init__.py │ ├── conftest.py # fixture 和钩子 │ ├── test_login.py │ └── test_order.py ├── reports/ │ └── ... # 测试报告输出目录 ├── requirements.txt ├── pytest.ini └── run.py # 统一入口脚本这套分层的逻辑很简单:config 管环境,core 管能力,data 管数据,testcases 管业务。测试用例里基本看不到 requests 的细节,也不会有环境地址写死的情况,看到的都是业务描述和数据引用。优势很明显:新人进来不用看全部代码,只需要知道“我要测什么接口,应该往 data 里加数据、往 testcases 里加用例”。2.2 核心模块的实现与理由先说 http_client.py,这是所有接口调用的底座。我封装它的原因有三个:统一处理基础请求头、统一异常捕获、统一日志记录。如果没有这一层,每个用例里直接调 requests.post,且不说代码重复,出了问题连最基本的请求耗时、返回状态码都没人记。# core/http_client.py import requests import time from core.logger import get_logger logger = get_logger(__name__) class HttpClient: def __init__(self, base_url, timeout=10): self.session = requests.Session() self.base_url = base_url self.timeout = timeout self.session.headers.update({ "Content-Type": "application/json;charset=UTF-8" }) def request(self, method, path, **kwargs): url = self.base_url + path kwargs.setdefault("timeout", self.timeout) start_ts = time.time() try: resp = self.session.request(method, url, **kwargs) logger.info(f"请求 {method} {url} 状态码={resp.status_code} 耗时={time.time() - start_ts:.3f}s") return resp except requests.RequestException as exc: logger.error(f"请求异常 {method} {url}: {exc}") raise def get(self, path, **kwargs): return self.request("GET", path, **kwargs) def post(self, path, **kwargs): return self.request("POST", path, **kwargs)注意这里我用的是 requests.Session 而不是裸调 requests.get/post。Session 会自动管理连接池,在跑大量用例的时候能复用底层 TCP 连接,实测同一套用例用 Session 比裸请求整体能快 20% 到 30%,这个提升在回放 200 条用例时体感非常明显。conftest.py 是 pytest 的灵魂。我在里面定义了一个全局的 client fixture,以及一个自动读取 yaml 测试数据的钩子:# testcases/conftest.py import pytest import yaml from core.http_client import HttpClient from config.settings import BASE_URL @pytest.fixture(scope="session") def client(): http_client = HttpClient(BASE_URL) yield http_client http_client.session.close() def load_test_data(file_name): with open(f"data/{file_name}", encoding="utf-8") as f: return yaml.safe_load(f) @pytest.fixture() def login_data(): return load_test_data("login_data.yaml")用 yaml 做数据驱动是我比较坚持的做法。对比 Excel 和 JSON,yaml 的书写成本低,支持注释,层级关系清晰,而且天然就是 Python 字典的结构。Excel 适合非技术人员维护大量数据,JSON 写起来括号多容易错,yaml 在这三者里是对测开同学最友好的一种。2.3 数据驱动和多环境切换的提效点数据驱动的核心不是“把数据放到 yaml 里”这个动作,而是“用例逻辑与测试数据分离”这种思维。举一个例子:登录接口需要验证正常登录、密码错误、账号不存在、账号锁定等多种场景。如果不用数据驱动,每条用例都是一段几乎一样的代码。用数据驱动之后,用例部分可以收敛成一条:# testcases/test_login.py import pytest class TestLogin: @pytest.mark.parametrize("case", [ {"desc": "正常登录", "payload": {"username": "admin", "password": "123456"}, "code": 0}, {"desc": "密码错误", "payload": {"username": "admin", "password": "wrong"}, "code": 1001}, {"desc": "账号不存在", "payload": {"username": "nobody", "password": "123456"}, "code": 1002}, ]) def test_login(self, client, case): resp = client.post("/api/login", json=case["payload"]) assert resp.json()["code"] == case["code"]这样写有一个明显的好处:以后新增场景只需要往列表里加一个字典,不用新增方法,也不会影响已有用例。而且用例名称、描述、预期结果全部一目了然,测试报告读起来就是业务语言,而不是一堆看不懂的 response body。多环境切换我放在了 settings.py 里解决。做法很简单:读取环境变量中的 RUN_ENV,默认走 dev,通过环境变量切到 staging 或 prod 配置,而不是每次改代码。我之前见过有人在代码里直接改 BASE_URL,然后不小心把带数据的测试环境给刷了,这种事故就是配置管理不规范导致的。用模板之后就再没出现过这种问题。3. 第二套模板:UI 自动化与端到端用例框架3.1 UI 自动化为什么更需要模板UI 自动化比接口自动化更容易失控,原因是它天然依赖环境:浏览器版本、驱动程序、页面加载速度、网络波动、弹窗遮挡,任何一个环节抖动,用例就可能挂。如果没有一套统一模板,每个用例自己处理等待、自己截图、自己拼元素定位,那最终结果就是“今天能跑,明天不能跑,没人知道为什么”。所以 UI 自动化模板的第一个目标不是写更多用例,而是建立稳定性机制。第二个目标才是复用。页面对象(Page Object Model,简称 POM)模式就是为此设计的:把页面元素定位和操作方法封装成类,测试用例只描述业务动作,不碰 CSS 选择器或 XPath。3.2 POM 模式与公共操作封装我在模板里用的是 pytest + Selenium,同时对关键公共操作进行了二次封装。为什么不用原生 find_element?因为原生方法在元素不存在时直接抛异常,不会等待,也不会留下现场信息,对于排查问题来说很不友好。封装后的方法长这样:# core/page_base.py import time from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class PageBase: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def click(self, locator): elem = self.wait.until(EC.element_to_be_clickable(locator)) elem.click() def fill(self, locator, text): elem = self.wait.until(EC.visibility_of_element_located(locator)) elem.clear() elem.send_keys(text) def get_text(self, locator): elem = self.wait.until(EC.visibility_of_element_located(locator)) return elem.text def screenshot(self, name): self.driver.save_screenshot(f"reports/screenshots/{name}_{int(time.time())}.png")这里的关键点是“显式等待”,而不是 time.sleep 这种固定等待。显式等待是条件满足就继续,不满足就等到超时;固定等待不管页面状态如何,都要傻等几秒,既慢又不稳定。实测下来,同样一版用例,把 sleep 全部换成显式等待之后,执行时间能缩短三分之一左右,稳定性还有明显提升。页面对象类则对应业务页面,比如登录页、订单页:# pages/login_page.py from selenium.webdriver.common.by import By from core.page_base import PageBase class LoginPage(PageBase): username_input = (By.CSS_SELECTOR, "#username") password_input = (By.CSS_SELECTOR, "#password") login_button = (By.CSS_SELECTOR, "button[type='submit']") def login(self, username, password): self.fill(self.username_input, username) self.fill(self.password_input, password) self.click(self.login_button)这样设计之后,用例层就变得非常薄:# testcases/test_login_page.py from pages.login_page import LoginPage class TestLoginPage: def test_user_login(self, driver): page = LoginPage(driver) page.open("/login") page.login("admin", "123456") assert page.get_current_url().endswith("/home")页面元素一旦变化,只需要改页面对应类里的一行定位,而不是满项目找在哪里用到了这个输入框。这就是 POM 的最大价值:把变化的范围限制在一个地方。3.3 稳定性治理:等待、截图、重试、失败处理UI 自动化最怕“偶现失败”。同一段用例,跑三遍挂一遍,还复现不了。我的经验是:遇到偶现失败,不要急着改业务逻辑,先看失败时的截图和录屏,八成是页面加载慢或者元素被弹出框遮挡。模板里内置了几个稳定性组件:第一个是失败自动截图。我在 pytest 的 hook 里加了失败处理,用例失败时自动保存截图并附加到 HTML 报告,这样一眼就能看出是定位错了还是页面崩了。第二个是失败重试机制。用了 pytest-rerunfailures 插件,对已知的不稳定用例设置 reruns=2,只对 UI 层用例开,接口层不建议开,因为接口层失败往往是真实逻辑问题,重试反而掩盖 bug。第三个是 headless 模式。CI 环境没有界面,运行到浏览器时如果没配置 headless,直接报错。模板里用一个配置开关控制,本地调试时开界面模式,CI 里切 headless。关于 Selenium 和 Playwright 怎么选,我个人的倾向是:如果团队已经熟悉 Selenium,生态成熟,没必要迁移;如果是新团队从零开始,直接上 Playwright 会更省事。Playwright 自带浏览器驱动管理、自动等待、录制、多标签页处理都更顺手。但这不代表 Selenium 过时了,很多老项目和开源生态都还在用 Selenium,关键是模板机制本身是通用的,底层引擎可以替换。4. 配套的日常提效工具箱4.1 日志和报告:不能只在本地跑通就没后续自动化框架跑完出一份报告,这算是基本要求,但很多模板会忽略日志的“可检索性”。我见过一些项目的 log 文件不分级别、不分时间,全塞在一个文件里,出了问题 Ctrl+F 找关键词找半天。模板里我用 logging 模块按天生成日志,并且把请求状态、响应摘要、耗时都打进去。这样即使测试报告没细看,也能通过日志快速定位到具体是哪个请求导致的失败。报告这块,接口层我用 pytest-html 加自定义摘要,UI 层用 pytest-html 附带截图展示。没有上太重型的测试平台,因为对多数项目来说,一个静态 HTML 报告加上错误日志,足够日常回归使用了。真要上平台,模板的用例结构也可以无缝对接,因为数据驱动和分层设计与 Allure 这类工具天然兼容。4.2 一条命令完成环境初始化模板要做到“拿来就能跑”,环境初始化就不能靠手点。我在根目录放了一个 setup 脚本,做的事情包括:创建虚拟环境、安装依赖、检查浏览器驱动、初始化报告目录。首次使用只需要执行:python setup.py pytest -v这里有一个细节:依赖版本不要全部放开,最好锁定大版本,尤其像 requests、pytest 这些核心包。我在 requirements.txt 里写的是:pytest>=7.0,<8.0 requests>=2.28,<3.0 PyYAML>=6.0,<7.0 pytest-html>=3.2,<4.0锁定大版本是为了避免自动化跑着跑着,因为某个库发布了不兼容的新版,整个框架突然崩掉。测开框架的第一要务是可控,不是追新。4.3 从模板到业务的 5 步落地流程模板本身不是最终交付物,业务用例才是。我复制模板到新项目时,通常会走一套固定的流程:复制模板目录,改项目名和包名;在 config 里新增被测环境的 base_url、账号、密钥等基础配置;根据接口文档或页面清单,在 data 目录或 pages 目录下补业务数据/页面对象;写第一批核心用例,跑通一条完整链路;提交代码,配置定时任务或 CI 触发。这五步走下来,一个项目的第一版自动化框架通常半天就能跑起来,剩下的时间全部投入业务用例编写。这才是模板真正“提效”的地方:它把最没价值的搭架子环节压缩到最小。5. 常见问题与排查技巧实录5.1 高频问题速查表下面这张表是我在实际推广模板过程中被问得最多的问题,连同排查思路一起列出来:现象可能原因排查方法与处理接口用例能跑到一半突然超时接口依赖的第三方服务不稳定查看日志里超时接口的耗时,确认是网络问题还是服务问题,对非核心依赖加 try 或延迟重试切换环境后大量用例失败配置没切换干净或测试数据污染检查所有 base_url 引用是否都走了配置,确认环境数据是否被上一次测试写脏UI 用例偶发找不到元素元素是异步加载的把固定 sleep 替换为显式等待,确认元素是否在 iframe/Shadow DOM 中浏览器启动后 driver 报版本不匹配浏览器自动升级,driver 版本落后模板支持自动下载匹配驱动,或固定浏览器版本报告里截图看不到报告目录被清理或路径含中文字符统一用绝对路径拼接报告目录,避免相对路径在不同目录下跑乱套数据驱动用例太多,单条定位困难用例 ID 不清晰给每条用例增加业务化 name,测试报告中用 label 标注模块和场景5.2 几个值得长期坚持的避坑习惯第一个习惯是“用例之间永远不要有执行顺序依赖”。我见过不少团队的用例是 A 登录、B 下单、C 支付,C 依赖 B 的订单号,B 依赖 A 的 token,一旦单独跑 C 就挂。这种用例集连基本的独立性都没有,根本算不上自动化回归。正确做法是每个用例自己完成前置数据准备,要么通过接口造数,要么在用例内部调用公共方法创建依赖对象。第二个习惯是“不要试图让 UI 用例覆盖所有异常场景”。异常分支和边界值优先用接口自动化覆盖,UI 层只保留用户最典型的高频操作路径。这样既能控制执行时间,也能减少维护成本。我见过团队一门心思把 UI 用例数量做到上千条,结果每天光是修失败用例就忙到下班,这就本末倒置了。第三个习惯是“模板要持续迭代,不能一沉不变”。比如之前我的接口模板用 requests,后来团队引入 gRPC,我就把 core 层扩展了一下,新增了一个 grpc_client。模板是活的东西,随着团队技术栈变化要持续维护,否则它的价值会随着时间慢慢衰减。6. 一点个人体会如果只挑一条经验分享给刚接触自动化的测开同学,我可能会说:先搭框架,再谈用例数量。框架决定了你的自动化能稳定跑多久,用例数量决定了你的自动化能覆盖多少业务。没有框架的用例是负债,有了框架的用例才是资产。这两套模板花了我不少业余时间打磨,但带来的回报也确实可观。现在不管接手什么新项目,我基本不用考虑“自动化架子怎么搭”这个问题,直接套模板,把精力集中在理解业务、设计用例上。这可能就是“提效”的本质——不是让你在同一件事上做得更快,而是让你把时间分配到更有价值的事情上去。后续如果有空,我可能会把两套模板整合到一个 CLI 工具里,做成“一条命令生成指定类型的自动化项目”,再结合任务调度做自动化巡检。等玩出效果了,再回来继续分享。