Selenium 做了这么多年 Web 自动化,我最深的一个感受是:很多人不是写不出脚本,而是不会把一个脚本组织成一套能长期跑的集成测试系统。单跑脚本谁都会,麻烦的是环境一换、页面一改、浏览器一升级,测试就稀碎。这篇是我在多个项目里反复打磨后沉淀下来的一套实践思路,从最基础的 WebDriver 用法讲起,一直聊到怎么把执行效率拉上去,让集成测试真正成为版本回归的“压舱石”,而不是 CI 里的定时炸弹。
我默认读者已经有一点点 Selenium 基础,至少知道它能打开浏览器、能找元素。如果没有也不用慌,我前两章会把最核心的概念用大白话重新捋一遍,后两章会深入到真正的效率瓶颈。如果你已经在写一些自动化用例,建议直接跳到第四章和第五章,那些踩坑的心得和效能方案,应该能帮你把维护成本和执行时间都打下来不少。
1. 项目概述与设计思路
1.1 集成测试到底测什么,为什么是 Selenium
单元测试解决的是一个函数、一个模块内部的正确性问题,但它管不了“模块和模块连起来之后是不是还正常”。集成测试就是干这个的——验证系统的各个组件组合在一起时,行为是否符合预期。
Web 集成测试有个特殊性:它的关注点不是页面长什么样,而是用户从界面触发一条业务链路后,数据、状态、结果是否正确流转。比如用户注册后能否登录,下单后订单状态是否正确,配置变更后列表是否刷新。这些链路单靠接口测试覆盖不到 UI 层的交互状态,单靠人工点又点不过来,所以需要一套能自动驱动的浏览器工具。
Selenium 就是这套工具里生态最成熟的一个。它的核心能力是通过 WebDriver 协议直接调用浏览器的原生自动化接口,也就是说 Selenium 不是靠截图对比、也不是靠模拟点击坐标来“骗浏览器”,而是真正驱动浏览器内核去执行操作。这样带来的好处是:支持的浏览器全、语言绑定多、社区资料多,出了问题基本上能找到前人踩过的坑。
但我要提醒一句:Selenium 适合的是“关键业务链路”的集成验证,不是所有页面都适合做成自动化。很多团队栽的跟头就是想把所有 UI 都自动掉,结果维护成本直接把收益吞了。做这套东西之前,先想清楚你的核心链路是哪几条、每一条的边界在哪、断言什么数据,这是设计测试套件的起点。
1.2 从功能测试脚本到测试系统的思路转变
刚开始学 Selenium 的人,通常写出来是这样的:
# 错误的脚本式写法:所有逻辑都堆在一个文件里 from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time driver = webdriver.Chrome() driver.get("https://dev.example.com/login") driver.find_element(By.ID, "username").send_keys("tester") driver.find_element(By.ID, "password").send_keys("pass123") driver.find_element(By.ID, "login-btn").click() time.sleep(3) # 固定等待,最坑的写法 driver.find_element(By.CSS_SELECTOR, ".user-name").is_displayed()这种直上直下的写法,演示挺好,生产环境不行。原因很简单:第一,time.sleep在机器快的时候浪费时间,机器慢的时候直接误报;第二,所有选择和操作散落在一个函数里,页面结构一改就得全局搜代码;第三,登录逻辑没法复用,每个用例都得重新写一遍。
所谓“从基础到系统”,核心是指从脚本思维切换到框架思维。我在实际项目里,通常把测试组织成四层:
- 基础能力层:封装 WebDriver 的创建、销毁、复用、配置加载;
- 页面对象层:每个页面对应一个类,页面上的元素定位和操作方法内聚在一起;
- 业务流层:把多个页面对象的操作串成“登录”“下单”“审核”这样的可复用业务动作;
- 用例层:只描述场景和断言,不关心元素定位细节。
这么分层之后,哪怕是新人接手,也只需要关注用例层的内容,不用在几千行代码里找元素选择器。后面我讲的实操,全部基于这套分层思想。
1.3 技术选型的底层逻辑
现在做 Selenium 自动化,语言选择上 Python 和 Java 各占半壁江山。Python 的优势是语法简洁、pytest 生态成熟,适合快速搭建和数据分析型断言;Java 在大型企业项目里更常见,因为测试代码能和业务代码混在同一个 Maven/Gradle 工程里,复用一些底层工具类更方便。
浏览器这块,我平时主力 Chrome,因为 DevTools 协议支持好、性能反馈快,日常调试方便;Firefox 在某些内网环境的兼容性更稳;Safari 和 Edge 主要用来做最终的兼容性抽查。不要一上来就在所有浏览器上跑全量回归,那样只会让 CI 时间爆炸,正确姿势是主浏览器跑全量、辅浏览器跑冒烟。
还有一个绕不开的话题:页面对象模式(Page Object Model,POM)。为什么大家都推荐它?因为测试代码和页面结构之间必须有一层缓冲。没有这层缓冲,页面从上到下找元素的操作散落在用例里,前端改了 class 名,你要在几十个用例里去改。有了页面对象,改一处就行。这个模式不是 Selenium 官方发明的,而是社区多年实践总结出来的,现在已经是做 Selenium 自动化的默认选项。
2. 核心细节解析与实操要点
2.1 WebDriver 的初始化、复用与生命周期
WebDriver 实例不是随便 new 一个就完事,它背后管理着一个真实的浏览器进程,成本很高。我曾经遇到一个项目,每个用例都新建 WebDriver,两百个用例跑下来,光启动浏览器的耗时就占了总执行时间的三分之一。
合理的做法是按需创建、会话复用、结束清理。pytest 里常用 fixture 来做:
import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture(scope="session") def driver(): options = Options() options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") driver = webdriver.Chrome(options=options) yield driver driver.quit()这里有几个关键点:scope="session"表示整个测试会话只启动一次浏览器,用例之间通过driver.get()跳转,而不是反复启停。--headless=new是无头模式,适合 CI 环境。--window-size一定不能省,否则页面可能用移动端布局渲染,元素定位全乱。--disable-dev-shm-usage在容器环境里必须加,否则临时文件共享内存不够会导致浏览器崩溃——这个我在 Docker 里实测踩过。
还有一点要特别强调:scope 设置为 session 后,用例之间是共享浏览器状态的。如果你的测试数据设计得不好,上一个用例的登录态会污染下一个用例。解决办法是每个用例开始时先访问一个清空态的页面,或者通过接口直接重置应用数据。集成测试里“测试数据隔离”和“浏览器会话复用”看似矛盾,实际上是两件不同维度的事,必须都处理到位。
2.2 元素定位策略:怎么选才稳
Selenium 提供八种定位方式:id、name、class name、tag name、link text、partial link text、CSS Selector、XPath。这么多方式,实际上日常能用的就三种:id、CSS Selector、XPath。name 和 class 的语义太弱,tag 几乎不用,link text 只适合纯链接场景,稳定性最差。
我的优先级原则:
- 有 id 用 id,id 应当唯一且稳定,除非前端用了动态 id;
- 没有 id 再看有没有
>.user-list-item:nth-child(3) .btn-action虽然依赖了结构位置,但只要列表顺序是接口返回的稳定顺序,就可以接受。比这个更脆弱的写法是用
contains(text(), '某某')去匹配按钮文字,一旦文案调整,用例就跟着爆。另外我要特别提醒:凡是前端经常加 class 做样式迭代的元素,定位属性尽量选择带业务语义的属性,比如
>from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC login_button = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, "#login-btn")) ) login_button.click()这里
element_to_be_clickable比visibility_of_element_located更严格,它会等元素可见且可点击。对按钮、链接这类可交互元素,我基本都用这个条件。要注意的是,等待时间不是越长越好。默认超时建议 8-10 秒,慢场景可以放宽到 15 秒,但要警惕一个现象:如果某个元素在 10 秒内始终不可见,大概率是页面报错了,而不是“还没加载完”。这时候你应该去查应用日志,而不是把等待时间改到 30 秒糊弄过去。这也是判断一个 Selenium 工程师到底熟不熟练的标准之一。
2.4 合理利用预期条件,少写循环轮询
很多新手遇到“某个状态需要几秒后才出现”的交互,会自己写
for循环加sleep去轮询。这个操作没有必要,Selenium 的 expected_conditions 已经把常见场景都封装好了。除了上面提到的可点击,还有几个高频的:presence_of_element_located:元素在 DOM 中出现,不需要可见;visibility_of_element_located:元素可见;staleness_of:元素从 DOM 中移除,适合判断页面跳转已开始;text_to_be_present_in_element:指定元素包含某文本;visibility_of_all_elements_located:一组元素全部可见。
集成测试中还有一种常见需求:等一个异步请求返回后,页面某个区域出现数据。这个时候与其轮询数据,不如等那个“请求中”的 loading 元素消失。用
invisibility_of_element_located配合 loading 标识,判断更准确。等待条件还有个小细节:显式等待默认 500ms 轮询一次。如果某些场景你希望响应快一点,可以自定义
WebDriverWait(driver, 10, poll_frequency=0.2),但不要搞太小,否则 CPU 空转而影响页面性能。2.5 常见操作中的动作与断言细节
集成测试里最多的操作是:点击、输入、选择、拖拽、切换窗口/iframe、文件上传、读取表格。前两者最简单,后几个很容易踩坑。
先说过 iframe。页面嵌了 iframe 之后,Selenium 默认找不到里面的元素,必须先切上下文:
driver.switch_to.frame("iframe-name") # 操作帧内元素 driver.find_element(By.ID, "code-input").send_keys("123456") # 操作完切回主文档 driver.switch_to.default_content()iframe 最容易踩坑的是切回主文档时忘记
default_content(),导致后续操作全在错误上下文。还有一点,iframe 的 id 可能是动态的,建议用 CSS 或 XPath 定位 iframe 元素后传给它。文件上传是另一个高发坑位。Selenium 有一个看似笨但实际最稳的方案:直接用
send_keys把本地文件路径发给<input type="file">元素。很多自动化框架用模拟 OS 弹窗的方式去操作文件对话框,那个非常不稳定,尤其是 headless 模式根本不弹窗。始终优先用 send_keys 方式,除非遇到的是非标准上传控件,才考虑用第三方工具模拟键盘操作。拖拽操作可以用 ActionChains:
from selenium.webdriver.common.action_chains import ActionChains source = driver.find_element(By.CSS_SELECTOR, "#drag-item") target = driver.find_element(By.CSS_SELECTOR, "#drop-zone") ActionChains(driver).drag_and_drop(source, target).perform()注意,HTML5 原生的拖拽在某些浏览器里 ActionChains 不生效,因为 Selenium 实现的是“鼠标按下—移动—释放”的模拟,不是 HTML5 dataTransfer。遇到这种情况,要么用 JavaScript 直接触发 drag 事件,要么降级为验证拖拽后的结果。不要在这个地方死磕。
页面跳转也是集成测试的高频场景:点击一个链接打开新 Tab,涉及窗口句柄切换:
original_window = driver.current_window_handle driver.find_element(By.LINK_TEXT, "打开新页面").click() WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) for window_handle in driver.window_handles: if window_handle != original_window: driver.switch_to.window(window_handle) break注意
number_of_windows_to_be这个条件比直接 sleep 要可靠得多,它会在新窗口打开后立刻返回,避免了“窗口还没创建完就去切换”的竞态问题。3. 实操过程与核心环节实现
3.1 搭建一个可复用的项目骨架
前面讲了设计思路和细节,现在直接上实操。我给出一套我常用的项目结构,基于 Python + pytest,你可以直接参考:
web_auto_test/ ├── conftest.py # pytest 全局配置与 fixture ├── config/ │ ├── __init__.py │ └── settings.py # 环境地址、账号密码、超时时间 ├── pages/ │ ├── __init__.py │ ├── base_page.py # 页面基类:封装等待、点击、输入 │ ├── login_page.py │ ├── dashboard_page.py │ └── order_page.py ├── testcases/ │ ├── __init__.py │ ├── test_login.py │ └── test_order_flow.py ├── utils/ │ ├── __init__.py │ ├── driver_factory.py │ └── data_builder.py # 测试数据构造 └── requirements.txt这套结构的好处是每个模块都有明确职责,不会出现“不知道某段代码该放哪”的情况。接下来我逐个讲关键文件怎么写。
3.2 驱动管理与页面基类的封装
driver_factory.py负责创建 WebDriver。我建议别把 ChromeOptions 散落在各个用例里,统一收口:from selenium import webdriver from selenium.webdriver.chrome.options import Options class DriverFactory: @staticmethod def create_driver(headless=True, lang="zh-CN"): options = Options() if headless: options.add_argument("--headless=new") options.add_argument(f"--lang={lang}") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_experimental_option("excludeSwitches", ["enable-automation"]) return webdriver.Chrome(options=options)excludeSwitches顺手去掉了“Chrome 正受到自动测试软件控制”的提示,有些页面会因为检测到自动化标记而改变行为,加上这行能降低干扰。base_page.py封装最常用的操作,所有页面类继承它:from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def find(self, by, locator): return self.wait.until(EC.presence_of_element_located((by, locator))) def click(self, by, locator): self.wait.until(EC.element_to_be_clickable((by, locator))).click() def input_text(self, by, locator, text): element = self.find(by, locator) element.clear() element.send_keys(text) def get_text(self, by, locator): return self.find(by, locator).text这里有个隐藏的好处:所有页面操作都走这层封装,将来一旦你决定把显式等待的默认超时从 10 秒改成 8 秒,只需要改一个地方。
页面类的写法,以登录页为例:
from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): username_input = (By.ID, "username") password_input = (By.ID, "password") login_button = (By.CSS_SELECTOR, "#login-btn") error_msg = (By.CSS_SELECTOR, ".error-tip") def login(self, username, password): self.input_text(*self.username_input, username) self.input_text(*self.password_input, password) self.click(*self.login_button) def get_error_message(self): return self.get_text(*self.error_msg)这种写法的价值在做集成测试时体现得很充分:登录这个动作在十几个用例里都要出现,如果每次都写四五行定位和操作,维护成本极高。封装成
login()之后,用例层一行调用即可。3.3 业务流复用与测试数据构造
集成测试和单元测试最大的不同,是测试数据要贴近真实业务。我强烈建议自己维护一个
data_builder.py,而不是在每个用例里硬编码账号和订单数据:import random import time class DataBuilder: @staticmethod def random_username(): return f"user_{int(time.time() * 1000)}" @staticmethod def order_data(product_id="P1001", quantity=2): return { "product_id": product_id, "quantity": quantity, "receiver_phone": "13800000000", "receiver_address": "XX市XX区测试路1号" }为什么不用固定数据?因为集成测试跑多了以后,库里可能会残留脏数据,第二次跑时“唯一性约束”直接报错。用时间戳生成随机标识,能极大提高测试的可重复性。
3.4 完整用例示例:一条核心链路
下面给出一条典型链路的完整用例:登录 → 创建订单 → 查看订单列表 → 断言状态。
import pytest from pages.login_page import LoginPage from pages.order_page import OrderPage from utils.data_builder import DataBuilder def test_create_order_flow(driver): # 登录 LoginPage(driver).login("tester", "pass123") assert "工作台" in driver.title # 创建订单 order_page = OrderPage(driver) order_page.create_order(DataBuilder.order_data()) # 断言列表出现新订单 new_order = order_page.get_latest_order() assert new_order["status"] == "待审核" assert new_order["product_id"] == "P1001"这个用例是典型的集成测试:它没有测单元层面的函数,而是验证真实用户从登录到下单到状态回显的完整链路。断言点尽量放在业务状态上,而不是页面样式上。比如断言“待审核”这个文本,而不是按钮是红色的。
3.5 把用例接入 CI 的关键配置
本地能跑通只是第一步,接入 CI 才是日常回归的常态。CI 环境里有个常见问题:容器内运行 Chromium 需要一堆系统依赖,少了哪一个浏览器都启动不了。
我常用的 Dockerfile 关键片段:
FROM python:3.11-slim RUN apt-get update && apt-get install -y \ chromium \ chromium-driver \ fonts-liberation \ libnss3 \ libnspr4 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libcups2 \ libxkbcommon0 \ libxcomposite1 \ libxdamage1 \ libxrandr2 \ libgbm1 \ libpango-1.0-0 \ libcairo2 \ libasound2 ENV PATH="/usr/local/bin:${PATH}"装完以后,在 conftest 里读取环境变量决定是否启用 headless:
import os from utils.driver_factory import DriverFactory @pytest.fixture(scope="session") def driver(): headless = os.getenv("HEADLESS", "true").lower() != "false" d = DriverFactory.create_driver(headless=headless) yield d d.quit()这样本地调试可以
HEADLESS=false pytest打开真实浏览器观察操作,CI 里默认无头运行,切换成本为零。4. 常见问题与排查技巧实录
4.1 定位不到元素,先排查这几件事
“NoSuchElementException”是 Selenium 世界最痛的异常。很多新手第一个反应就是加等待,但我建议按顺序排查,能省下大量时间:
- 页面真的加载了吗,还是应用直接报错白屏了;
- 元素是不是在 iframe/Shadow DOM 里,在的话需要先切;
- 元素是不是需要滚动才可见,headless 窗口高度不够也会定位失败;
- 是不是有多个同类型元素,
find_element默认返回第一个,可能不是你要的; - 定位器本身是不是过期了,前端版本更新后 class 变了。
排查时有个非常好用的技巧:在报错那一刻把页面状态截屏保存。pytest 的 fixture 可以这样挂:
@pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs["driver"] driver.save_screenshot(f"report/{item.name}.png")掉进失败现场能直接看到页面长什么样,往往比看日志快得多。
4.2 等待超时与竞态条件的判断口径
等一个元素超时,不一定就是页面慢。我在项目里总结过一个判断口诀:先看元素存不存在,再看元素是否可见,最后看是否可点击。对应到代码里,就是依次尝试三种 expected_conditions,就能精确定位卡在哪个环节。
竞态条件是最隐蔽的问题。典型的场景是:点击按钮后,页面先出现旧数据,再异步刷新出新数据。如果你在点击后立刻断言内容,可能断的是旧数据,测试时好时坏。
解决思路是等待“异步请求完成”的明确信号,比如接口响应后页面调用某个特定 DOM 状态,或者用一个更稳定的标志——请求期间出现的 loading 层消失。我在页面上喜欢找一个临时的加载遮罩层,用
invisibility_of_element_located等待它消失后再继续操作:self.wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, "#loading-mask")))这套方案比等新数据出现更可靠,因为它不依赖业务数据本身,是纯粹渲染层的信号。
4.3 浏览器崩溃、会话丢失与资源泄漏
浏览器进程在长时间跑回归时,偶尔会直接崩掉,典型表现是日志里出现“session not created”或者 WebDriver 相关的异常。这类问题没有定海神针式的解法,我的经验是两条腿走路:
第一,降低资源消耗。headless 模式下加上
--disable-gpu,减少渲染进程的额外开销;不要同时跑几十个无头浏览器在同一台机器上,容器内存不够最容易触发崩溃。第二,设置失败重试。pytest-rerunfailures 插件可以给短时抖动留出缓冲,我通常只在 CI 里启用:
pytest --reruns 2 --reruns-delay 5但注意:重试是兜底手段,不是遮羞布。如果同一个用例 5 次里有 3 次需要重试才通过,说明问题不是环境抖动,而是用例或测试数据本身不稳定,必须回头查。
4.4 调试利器:截图、日志与录制回放
我个人的调试流程通常是:定位失败 → 看截图 → 加日志 → 手动生成页面元素快照。
前面的 hook 已经把失败截图存下来了,这是第一步。第二步是在关键步骤打日志,观察操作走到了哪里。第三步更进阶,当你想确认“页面在某个时刻有哪些元素可用”时,可以把整个 DOM 快照打印出来:
from selenium.webdriver.common.by import By page_source = driver.page_source with open("dom_snapshot.html", "w", encoding="utf-8") as f: f.write(page_source)配合浏览器的 DevTools 打开快照文件,就能精确还原现场。对于特别难以复现的偶发问题,我会保留当时的网络日志和浏览器控制台日志:
logs = driver.get_log("browser") for log in logs: print(log)浏览器控制台的 JS 报错,往往比测试框架的异常更能揭示页面内部的失败根因。
4.5 实际问题速查表
现象 可能原因 推荐的排查动作 元素定位不到 iframe 未切换 / 定位器过期 / 页面报错 先截图,再检查 DOM 快照 用例偶发失败 竞态条件 / 固定 sleep 过短 改用显式等待 loading 消失 浏览器进程残留 异常退出未调用 quit fixture 用 yield 后加 try/finally 运行越来越慢 浏览器窗口未复用 / 日志膨胀 检查会话作用域,定期清理 CI 里启动失败 容器缺少系统依赖 补 Chromium 库,查 Dockerfile 无头模式行为异常 窗口尺寸过小 / 动画影响 固定宽高,禁用动画 这张表是我实际项目中排查频率最高的六类问题,其他冷门问题大多都能从这三个源头延伸出来:上下文没切对、时序没对齐、资源没管好。
5. 效能突破:从串行到并行的关键手段
5.1 定位性能瓶颈:先算清楚时间花在哪
很多团队跑一套几百条的集成测试要一两个小时,第一反应是上并行,但我不建议直接上。先搞清楚时间分布,再决定优化方向。方法很简单:pytest 加
--durations=10,跑完会列出最耗时的十个用例。实际数据往往是这样:80% 的耗时集中在少数几个用例上,比如涉及上传大文件、导出报表、发送通知邮件的链路,一个用例可能占 5 分钟。这种情况下,优先优化单个用例的等待策略,比上并行收益更大。
等待策略优化的核心思路前面已经说过:消灭固定 sleep。一个用例里有 5 处
sleep(3),一轮就是 15 秒浪费,20 个用例下来就是 5 分钟。全部换成显式等待后,通常能省掉 40% 以上的执行时间。5.2 浏览器复用与用例分发策略
如果已经消灭了不必要的等待,执行时间依然很长,就需要考虑并行。这里的瓶颈主要在资源:一台机器上同时开太多浏览器,CPU、内存都会爆。
两种并行思路:
- 多进程并行:用 pytest-xdist 在单机多核上并行跑用例。每个 worker 一个独立进程,各持一个浏览器实例。设并发数时,不要拍脑袋定 8、12,先看机器配置。我的经验是 4 核 8G 的机器开 3-4 个并行浏览器比较稳,再多内存就吃紧。
- 多机分布式:用 Selenium Grid 把用例调度到多个节点执行。适合用例量非常大的团队,基础设施成本高一点,但扩展性最好。
启动方式很简单:
pytest -n 4 --dist=loadscope--dist=loadscope会把同一个模块里的用例分到同一个 worker,避免同一页面类操作在两个进程中相互干扰。这比默认分配策略要稳,因为某些依赖共享状态的测试模块不会跨进程打架。5.3 容器化与资源配额的实际效果
我现在的项目里,并行执行跑在 Docker Compose 编排的容器集群里。每个容器里跑一个 Selenium 会话,资源配额按单容器限制:
# docker-compose.yml 片段 services: chrome-worker: image: custom-chrome-image deploy: resources: limits: memory: 2G reservations: memory: 1G配合 pytest-xdist 的多进程分发,外部调度器负责把测试任务切分到每个容器。实测下来,200 条用例从串行的 90 分钟降到并行的 22 分钟,提升差不多 4 倍。这个提升不是线性的,因为资源调度和数据隔离也是成本。
容器化带来的另一个好处是环境一致性。以前经常出现“本地跑过,CI 挂了”的问题,容器化之后,开发、测试、CI 用的是同一镜像,环境差异直接被抹平。
5.4 按依赖关系分组与冒烟回归分级
并行不是万能的。如果用例之间有强依赖——比如 B 用例必须等 A 用例创建的数据——那么并行反而会引入随机失败。解决方法是测试设计阶段就规避用例间依赖。
具体做法:让每个用例自主创建自己的前置数据,执行完尽量清理。比如一个订单用例,不依赖别的用例先建单,而是自己调接口生成唯一订单号,用完标记作废。这样用例天然可以并行。
另一个维度是分级。我一般把测试分成三层:
- 冒烟层:核心登录 + 一条最简单链路,10 分钟内跑完,每次代码合并必跑;
- 回归层:核心业务链路全量,每天定时跑,或者发版前跑;
- 扩展层:低频、慢速、兼容性用例,每周跑一次。
这么一分,CI 反馈速度大幅提升,开发合并代码等的结果时间从两小时缩短到十分钟。很多团队不是测试跑不动,而是把所有用例塞成一个大包,最后搞得谁都不敢动桩。
5.5 数据、接口与 UI 融合:性能与稳定的终极平衡
做到最后你会发现一个规律:集成测试里,真正值得用 UI 去验证的,是那些涉及前端状态流转和真实用户交互的核心链路。数据准备、状态清理、前置校验这些环节,完全可以通过后端接口来做,速度比 UI 快一个量级。
我在项目里的模式是:接口准备数据 + UI 验证流程 + 接口断言结果。举一个实际例子,测试“订单审核通过后列表状态更新”:
- 通过接口创建一张订单,直接拿到一个确定状态的初始数据(接口调用耗时毫秒级);
- 用 UI 操作进入审核页、点击通过(验证 UI 交互链路);
- 通过接口查询该订单的最终状态并断言(绕开 UI 获取最终结果,避免不确定性)。
这样一条用例的耗时可能只有纯 UI 做法的三分之一,而且稳定性明显更高。不要觉得这和 Selenium 无关——Selenium 的价值正在于把“最需要 UI 的那段链路”测准,其余部分交给更快的工具去做。
6. 维护文化与持续演进
6.1 测试代码也是产品代码,对待它的态度要一样
很多团队自动化测试做几个月就废弃了,原因不是工具不行,而是把测试代码当成了二等公民。没人 review,没有规范,没有文档,前端改版时定位器全废,大家嫌维护麻烦,慢慢就不跑了。
我的观点是:测试代码的质量要求,甚至应该高于业务代码。因为业务代码有线上问题兜底,测试代码出问题,直接后果就是失去对回归的信任。宁可少写用例,也要保证每条用例稳定、可读、可维护。
几个具体的规范:页面对象里不要出现断言;用例里不要出现元素定位;测试数据统一从工具类获取;每个用例必须有明确的业务场景描述 docstring。这些约束看着琐碎,但长期执行下来,团队的自动化资产才能保值。
6.2 定位器维护与前端协作的实践
定位器失效是 Selenium 维护成本的大头。我踩过几次坑之后,摸索出一个相对靠谱的协作方式:测试团队和前端团队约定,关键交互元素加上
>