Selenium Web自动化测试实战:从环境搭建到框架设计 📅 发布时间:2026/9/8 5:43:37 👁 浏览次数: 1. 先说清楚Selenium到底是测试工具还是爬虫工具我最早接触Selenium是因为一个特别常见的误解——以为它是爬虫工具。当时有个需求要抓某个动态渲染的网站用requests拿不到数据搜索一圈所有人都在说“用Selenium”于是我也跟着用了。用了一段时间之后才明白这个定位其实不完全对。Selenium的官方身份是Web自动化测试框架它的本职工作是模拟用户在浏览器里的操作然后验证页面行为是否符合预期。爬虫只是它的“副业”或者说是自动化能力的一个衍生用途。理解这一点很重要。因为当你把它当“测试工具”来用的时候你会更关注稳定性、可重复性、元素定位的准确性、等待条件的可靠性而当你把它当“爬虫工具”来用的时候你只会关心数据有没有抓到。这两种心态会导致完全不同的用法。比如测试场景里你必须要写显式等待必须处理元素找不到的异常必须考虑页面并发渲染的顺序但在“能跑就行”的爬虫脚本里这些基本都是糊弄过去的。这也是很多Selenium初学者跑了两三天就开始踩坑的根本原因——他们没搞清楚Selenium的底层逻辑是“验证”而不是“抓取”。再说得直白一点Selenium是一个WebDriver协议的实现。它启动浏览器、打开页面、执行JavaScript、点击按钮、输入文本、读取页面状态全部都是通过一套标准化的驱动接口来完成的。你在Python里写driver.find_element(...)本质上是通过HTTP协议告诉浏览器驱动“帮我去页面上找一个元素”然后浏览器驱动再去调用浏览器内核执行这个操作。这不是什么黑魔法它就是一套远程控制协议。所以想真正掌握Python Selenium的自动化Web测试你需要搞清楚的无非是四件事环境怎么搭、定位怎么写、等待怎么等、异常怎么处理。这四句话听起来简单但每一项拆开都有不少可讲的细节。这篇文章里我不打算给你抄一段“官网示例”然后敷衍了事我会站在实际项目的角度把Selenium自动化测试里最核心、最容易翻车、也最值得花时间的部分全部罗列出来。如果你现在的水平是Python基础语法还不熟不太清楚怎么安装依赖包分不清WebDriver和Selenium的关系或者已经能跑通Demo了但一遇到动态页面、弹窗、iframe、文件下载就开始抓狂——那这篇文章应该能把你的进度往前推一大截。2. 环境搭建这一步坑比你想的多得多2.1 Python环境本身怎么搞才省心说句实话Selenium本身装起来不费劲pip install selenium一行命令就够了。真正让你头疼的是Python环境、浏览器驱动、IDE配置这三样东西的组合问题。先聊Python环境。我见过很多初学者卡在“装Python”这一步其实不是Python难装是他们不知道该装哪个版本、装完了怎么验证。如果你是Windows系统去Python官网python.org下载安装包的时候有一个非常关键的选项——安装时务必勾选“Add Python to PATH”。这个选项不勾选装完了之后在命令行里敲python会提示“不是内部或外部命令”然后你就开始怀疑人生了。装完之后打开命令行cmd或PowerShell输入下面的命令验证python --version pip --version能正常输出版本号恭喜你环境的第一关过了。然后就是虚拟环境的问题。我强烈建议你从第一天开始就养成用虚拟环境的习惯。这不是折腾这是保命。因为真实项目里你不太可能只装一个selenium你还要装pytest、requests、webdriver-manager、pandas这些包之间有版本依赖关系。你要是全装到系统Python里哪天某个包升级把另一个包搞坏了你都不知道是谁干的。venv就是给你每个项目单独隔离一套环境互相不干扰。在项目目录下执行python -m venv venvWindows下激活虚拟环境venv\Scripts\activateMac/Linux下激活虚拟环境source venv/bin/activate激活之后命令行前面会出现(venv)的标识这时候你再安装Selenium它就会被安装到这个独立的虚拟环境里。2.2 Selenium和WebDriver的版本匹配这是让无数人崩溃的一个坑。Selenium这套东西分两部分一部分是Python库selenium包另一部分是浏览器驱动如ChromeDriver、GeckoDriver、EdgeDriver。你需要安装跟你浏览器版本精确匹配的驱动然后通过代码里的webdriver.Chrome()去调用它。早期的Selenium用法是from selenium import webdriver driver webdriver.Chrome(executable_pathpath/to/chromedriver)如果你用的是Selenium 4.x现在是主流版本写法稍微变了一点from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(executable_pathpath/to/chromedriver) driver webdriver.Chrome(serviceservice)不过说实话手动下载驱动、维护版本匹配这件事非常低效。浏览器是自动升级的今天更新了Chrome明天驱动就失效了然后你的脚本莫名其妙报SessionNotCreatedException原因是Chrome binary version mismatch翻译成人话就是“浏览器版本太新驱动不认识它”。我的建议是直接用webdriver-manager这个库来自动管理驱动版本pip install webdriver-manager然后代码变成这样from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)这样它每启动前都会自动检查本地驱动是否与浏览器版本匹配不匹配就自动去下载正确版本从此告别“驱动版本不匹配”这个问题。这个是提升幸福指数最明显的一个操作。注意国内有些网络环境下webdriver-manager从Google官方服务器下载驱动可能比较慢如果卡住了可以配置使用镜像源这个主要看你实际的网络情况。2.3 IDE选哪个直接影响你排查问题的效率编辑器选择上社区里主要两个派系PyCharm和VS Code。我的个人感受是PyCharm适合写测试工程、跑pytest、看测试报告尤其是社区版免费已经完全够用。VS Code的优势是轻量、插件生态丰富适合写小脚本、做快速验证。如果你用PyCharm要注意一个细节新建项目的时候一定要选择已经配置好Python解释器。最好是一开始就用上面建好的虚拟环境venv目录下的python.exe不然你命令行里装了一堆包IDEA里却全标红根本导入不了selenium。确定解释器路径的操作路径是File → Settings → Project → Python Interpreter把你的虚拟环境路径加进去。VS Code则需要手动配置Python解释器按CtrlShiftP输入Python: Select Interpreter选择虚拟环境的Python版本。这一步配置好之后你写from selenium import webdriver的时候就不会看到红色波浪线了。红波浪线看着是小问题但非常影响判断因为它会干扰你对“真正错误”的感知。3. 核心原理拆解Driver、定位、交互到底是怎么运作的3.1 WebDriver是怎么控制浏览器的你要理解Selenium的工作逻辑只需要记住两个角色客户端和服务端。客户端你的Python脚本里面调用selenium.webdriver这个API。服务端浏览器驱动Chromedriver启动的WebDriver服务。你的脚本通过HTTP请求给WebDriver发指令“打开https://example.com”、“找到ID为username的元素”、“输入一段文字”、“点击这个按钮”。WebDriver收到指令之后用浏览器原生的自动化调试协议比如Chrome是DevTools Protocol简称CDP去驱动浏览器做这些动作。这就带来一个非常重要的推论Selenium的等待、查找、执行都受浏览器当前状态的直接影响。页面还在加载的时候你去找元素元素还没渲染出来必然定位失败页面弹了个原生弹窗你的脚本卡在那里也不会有任何报错就一直等着。这些都不是Selenium本身的问题而是操作时机和页面状态的问题。3.2 八种元素定位方式优先级怎么排元素定位是所有Web自动化的根基。Selenium提供八种定位方式find_element(By.ID, id值)find_element(By.NAME, name值)find_element(By.CLASS_NAME, class值)find_element(By.TAG_NAME, 标签名)find_element(By.XPATH, xpath表达式)find_element(By.CSS_SELECTOR, css选择器)find_element(By.LINK_TEXT, 链接文字)find_element(By.PARTIAL_LINK_TEXT, 链接文字片段)实际项目里用得最多的是ID、CSS_SELECTOR、XPATH这仨。优先级的判断标准很简单标识越唯一优先级越高。如果一个元素有id优先用id因为id在页面里是唯一的没有id但有class且有业务含义可以用CSS_SELECTOR两者都不好使再上XPATH。很多人有个误区就是遇到定位不到的元素就无脑上XPath然后写出一长串//*[idapp]/div[2]/div[3]/div[1]/span这种“脆皮”表达式。这种写法极度依赖页面DOM结构前端稍微改一版你的定位就失效了。我更推荐的做法是优先给前端提要求给元素加测试专属的标识比如>from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait WebDriverWait(driver, 10) element wait.until( EC.element_to_be_clickable((By.ID, submit-btn)) )常用的条件还有EC.presence_of_element_located元素出现在DOM中EC.visibility_of_element_located元素可见EC.element_to_be_clickable元素可点击可见且不禁用EC.frame_to_be_available_and_switch_to_itiframe可切入EC.alert_is_present有弹窗出现3.4 和页面交互时最容易忽略的两个动作Selenium的基本交互就是click()、send_keys()、submit()、clear()。但在实际测试里我发现真正的问题往往出在点击之前没有检查元素是否真的被其他元素遮挡。前端常见的弹窗遮罩、广告浮层、懒加载占位会让元素哪怕已经出现在DOM里也被其他层挡住了此时click会抛出ElementClickInterceptedException或者“静默失败”。这个问题的排查方式是在点击前用element.is_enabled()、element.is_displayed()判断更彻底的办法是在点击前滚动到元素位置让它的可见区域真正暴露出来我习惯加一句driver.execute_script(arguments[0].scrollIntoView();, element)。send_keys之前没有清空原有内容。输入框经常自带默认值或者上一次输入残留直接send_keys会把内容追加在后面而不是替换。稳妥的做法是element.clear()之后再send_keys或者用ctrla全选替换element.send_keys(Keys.CONTROL, a) element.send_keys(新内容)这些细节看似基础但在真实测试脚本里出现的频率非常高。4. 实战场景从写脚本到处理动态验证码的过程记录4.1 一个登录用例是怎么从无到有的为了让你看得更直观我用一个典型的登录场景完整走一遍流程。需求是这样的打开一个测试站点输入用户名和密码点击登录验证是否跳转到个人中心。第一步初始化浏览器并打开目标URLfrom selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() options.add_argument(--start-maximized) driver webdriver.Chrome(serviceservice, optionsoptions) driver.get(https://example.com/login)建议启动后直接最大化窗口很多前端样式在非最大化状态下会把部分元素挤出可视区造成元素不可见不可点。第二步定位用户名、密码输入框填入内容wait WebDriverWait(driver, 10) username_input wait.until( EC.presence_of_element_located((By.ID, username)) ) username_input.clear() username_input.send_keys(test_user) password_input driver.find_element(By.NAME, password) password_input.clear() password_input.send_keys(passw0rd)这里我用显式等待等待用户名输入框出现密码框因为和用户名同属一个form且出现在同一渲染批次里就直接用find_element不必每次都显示等待。第三步点击登录按钮login_btn wait.until( EC.element_to_be_clickable((By.XPATH, //button[contains(text(), 登录)])) ) login_btn.click()第四步验证登录是否成功wait.until( EC.url_contains(/dashboard) )只要URL从/login跳到了/dashboard说明登录成功。当然也可以直接断言页面里某个“欢迎xxx”的元素出现。4.2 滑块验证码自动化测试怎么绕过“人机校验”这个点是很多搜索记录里反复出现的痛点——“自动化selenium网页拼图验证怎么自动化”、“selenium图片滑块验证”。先说一个基本态度滑块验证码出现的目的是区分人与机器测试场景里正经的解决方案是走“测试环境开关”或找后端加白名单。但如果你的测试环境和真实环境一样都有滑块那确实需要技术手段去模拟。滑块拼图验证的核心逻辑其实不复杂画布上有一张背景图、一个缺口拼图、一个可拖动的滑块。你要做的第一步是计算出滑块需要水平拖动的距离第二步是模拟人类拖动的轨迹把滑块拖过去。距离怎么算最常用的方式是利用PIL图像处理库分析两张图片的像素差异找到缺口位置。思路是获取无缺口的完整图片和有缺口的背景图片逐像素比对像素差异最大的区域的x轴坐标就是缺口位置。上面这张图一般前端会把它作为canvas绘制出来你可以通过element.screenshot()把canvas截下来或者通过canvas.toDataURL()拿到base64图片数据然后用PIL解析。计算得到位移之后模拟拖动的代码大致是from selenium.webdriver.common.action_chains import ActionChains import time slider wait.until(EC.presence_of_element_located((By.CLASS_NAME, slider-btn))) ActionChains(driver).click_and_hold(slider).perform() # 分段移动模拟人的轨迹 step distance / 10 for i in range(10): ActionChains(driver).move_by_offset(step, random.uniform(-1, 1)).perform() time.sleep(0.05) ActionChains(driver).release().perform()模拟人类拖动轨迹很关键。你要是用move_by_offset一次性拖到位后台的风控模型很容易判定你是机器因为人的手速不可能那么均匀那么直。分段移动、在移动过程中加一点y轴抖动、在接近终点时稍微收一下速度这三个点模拟出来通过率会明显高一些。这里必须提醒一句滑块验证属于网站的“人机识别”防护绕过它本身就处在灰色地带。测试环境里调试自动化脚本没问题但你拿这套技术去刷票、抢购、批量注册任何时候都不推荐。做技术的人应该清楚能力边界在哪里。4.3 文件下载、文件上传的自动化处理热搜词里有个“selenium怎样使文件下载完成之后才进行下一步”这个问题出现的频率太高了。很多人用Selenium点击了下载按钮然后代码直接往下走结果文件还没下完就去做后续处理了必然报错。要处理“等待下载完成”Selenium本身是没有内置下载完成检测的。常见做法是设置浏览器下载目录然后在代码里循环检查文件系统import os import time download_dir /path/to/download def is_download_finished(directory, timeout30): start time.time() while time.time() - start timeout: files os.listdir(directory) # 有 .crdownload 或 .tmp 后缀说明还没下完 unfinished [f for f in files if f.endswith((.crdownload, .tmp))] if not unfinished and files: return True time.sleep(1) return False配合ChromeOptions设置下载目录prefs { download.default_directory: download_dir, download.prompt_for_download: False, download.directory_upgrade: True, safebrowsing.enabled: True, } options.add_experimental_option(prefs, prefs)判断逻辑很简单只要目录里还有.crdownload后缀的临时文件就说明下载尚未完成不要开始后续操作。文件上传就更简单了。页面上的input typefile元素直接用send_keys()把本地文件的绝对路径传进去就行file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) file_input.send_keys(/Users/username/test_report.pdf)不需要去点那个“上传”按钮再去找系统弹窗WebDriver协议规定文件输入框可以通过send_keys直接设置文件路径。很多新手不知道这一点反而卡在“怎么操作系统文件选择框”上。记住这招能省你很多时间。4.4 iframe和window切换你找的元素可能在“另一个世界”页面里还有一类非常隐蔽的坑——iframe。iframe相当于页面里嵌套了另一个页面如果你不切进这个iframe你在外层怎么用find_element都找不到里面的元素哪怕XPath写得再精准也没用。处理方式# 等待iframe可切换 wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, frameId))) # 此时操作iframe内的元素 inner_btn driver.find_element(By.ID, inner-button) inner_btn.click() # 操作完后切回主文档 driver.switch_to.default_content()窗口切换也有类似的逻辑。你点击一个链接打开了新标签页此时driver还在旧页面你必须显式切换driver.switch_to.window(driver.window_handles[-1])window_handles是所有窗口句柄的列表-1代表最后一个新打开的窗口。切换窗口之后还要用driver.close()关闭多余窗口的习惯建议从一开始就养成不然开着开着几万个标签页堆积在内存里最后整个浏览器直接卡死。5. 测试代码的组织方式从脚本到自动化测试框架5.1 早期我犯过的错把所有逻辑写在一个main函数里如果你只是临时跑一个脚本把selenium操作全部写在一起问题不大。但在真实项目里你要维护的是几十上百个用例这时候脚本式写法带来的问题就非常突出了重复代码多、用例之间相互影响、报错信息不可读、跑一次要人盯着看。比如你有五个用例都要“登录”这个前置动作如果每个用例里都复制粘贴一遍登录代码有一天登录输入框的id变了你要改五个地方还很容易漏改。这就是为什么要从“脚本”走向“框架”把公共能力抽出来。我的习惯是把代码分成几层页面对象层Page Object Model每个页面封装成一个类页面的元素定位和操作方法都在类里面。测试用例层每个以test_开头的函数是一个用例负责组织操作步骤和断言。公共组件层比如driver初始化、等待封装、截图工具、日志工具。5.2 Page Object Model页面即对象先看一个最简单的首页对象class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.NAME, password) self.login_button (By.XPATH, //button[contains(text(), 登录)]) def enter_username(self, username): element self.driver.find_element(*self.username_input) element.clear() element.send_keys(username) def enter_password(self, password): element self.driver.find_element(*self.password_input) element.clear() element.send_keys(password) def click_login(self): element self.driver.find_element(*self.login_button) element.click()将来如果定位方式变了只需要在这个类里改元组里的值所有调用这个类的用例都会自动更新。这让维护成本大幅下降。5.3 pytest把Selenium包装成真正的自动化测试pytest是Python生态里最主流的测试框架。它配合Selenium的用法很简单import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service pytest.fixture def driver(): service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() options.add_argument(--headless) # 如果需要无头模式跑 driver webdriver.Chrome(serviceservice, optionsoptions) yield driver driver.quit()fixture是pytest的核心概念。上面这段代码就是一个fixture它会在每个测试用例执行前创建driver用例结束后自动关闭driver避免浏览器实例泄漏。然后写用例def test_login_success(driver): page LoginPage(driver) page.enter_username(test_user) page.enter_password(passw0rd) page.click_login() assert driver.current_url https://example.com/dashboard配合pytest -v跑起来控制台会输出每个用例的名称和执行结果绿点是通过红点是失败。失败的时候还能自动截图保存现场这个是排查问题的利器。5.4 失败截图为什么是必加项UI自动化最痛苦的事情是什么不是脚本报错而是脚本报错你看不到页面当时的状况。等你登录到远程机器上看的时候页面状态早就变了。所以在一套完整的框架里失败自动截图几乎是必须的。pytest里可以用conftest.py定义pytest_runtest_makereport钩子import pytest from pathlib import Path pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: screenshot_dir Path(screenshots) screenshot_dir.mkdir(exist_okTrue) driver.save_screenshot(str(screenshot_dir / f{item.name}.png))这样一旦用例失败就会自动保存一张失败时的页面截图到screenhots目录方便你快速定位是页面崩了、元素没加载出来还是断言条件不满足。5.5 无头模式与并行执行真实项目里跑UI自动化通常不会开着浏览器让你肉眼盯着。CI环境持续集成里跑测试浏览器界面根本没有硬件加速需要用到--headless无头模式options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage)注意无头模式下要注意两点一是无头模式依然是真实浏览器内核只是不渲染窗口所以CSS渲染结果和正常模式基本一致二是无头模式对动画、懒加载的处理有时和正常模式有细微差异如果发现某个用例只在某种模式下失败先别急着怀疑浏览器优先怀疑是不是等待条件写得太脆了。并行执行可以用pytest-xdist插件pip install pytest-xdist pytest -n 4这样4个用例可以同时在4个线程里跑。并行虽然能显著缩短总体执行时间但对被测系统的并发承受能力也是个考验如果被测系统扛不住并行反而会造成大量误报所以加并行要谨慎。6. 踩坑复盘这几个错误我用坏了好几个浏览器Driver6.1 坑位一定位方式写得“太聪明”有阵子我为了图省事经常用contains(text(), xxx)去匹配元素比如driver.find_element(By.XPATH, //div[contains(text(), 确认)])刚开始一切正常后来前端结构调整页面里多个元素都包含“确认”两个字find_element默认返回第一个匹配的元素结果点了不该点的按钮测试用例用例全都串了。从那以后我给自己立了个规矩能用位置关系表达的不要依赖文本必须依赖文本的时候用精确匹配而不是containsdriver.find_element(By.XPATH, //button[text()确认])如果是动态生成的部分匹配至少要拼上父级节点做范围限定。6.2 坑位二点击失效但不报错有一种非常磨人的情况定位到了元素click也没有抛异常但页面没有任何反应。这种“静默失败”比直接报错更让人崩溃。排查思路是有层次的。先看元素是不是被遮挡用element.get_attribute(class)看样式类里是不是有disabled状态再看是不是有元素正在覆盖它可以用JavaScript执行一次强制点击做交叉验证driver.execute_script(arguments[0].click();, element)如果强制点击生效说明就是遮挡问题回头检查等待条件是否合理或者需要先关闭弹层/遮罩。如果强制点击也不生效那就是元素绑定的JavaScript事件没有正确初始化这种情况要考虑刷新页面或者调整操作顺序。6.3 坑位三浏览器进程残留Windows环境下测试脚本异常退出之后chromedriver进程经常不会自动结束还会连带一堆chrome的僵尸进程留在后台。再跑脚本的时候新启动的Chrome会被残留进程影响最常见的问题是端口被占用导致WebDriver启动失败。这时候最简单的处理方式就是脚本入口处加一个进程清理逻辑taskkill /F /IM chromedriver.exe /T taskkill /F /IM chrome.exe /TLinux/Mac下对应的是pkill -f chromedriver pkill -f chrome更好的做法是代码里用finally或fixture的teardown保证driver.quit()一定会被调用。6.4 坑位四懒加载导致元素渲染时间不可控现在的网页前端大量使用懒加载图片、下拉列表、列表数据往往要滚动到可视区域才会发起网络请求加载。你直接driver.get(url)之后立刻去定位页脚的数据十次有九次找不到。解决方案是滚动页面触发加载再等待条件满足driver.execute_script(window.scrollTo(0, document.body.scrollHeight);)或者更细致一点指定滚动到某个具体元素的位置element driver.find_element(By.CSS_SELECTOR, .footer) driver.execute_script(arguments[0].scrollIntoView();, element)滚动之后不要立刻去找元素加一个短暂的显式等待比如等待目标元素可见再去操作。数据加载的场景里我习惯用“数据占位符出现→数据占位符消失→断言出现内容”这种三步判断比单纯等固定几秒靠谱得多。一般加载状态会有loading动画或者占位符等待它消失再断言稳定性会明显提升。7. 最后分享几个我用了一年才养成的测试习惯说几个不在任何教程里、但实际帮了我大忙的习惯。第一每一条测试用例必须能独立运行不依赖其他用例的执行顺序。什么叫“不依赖”就是你可以单独跑pytest test_login.py::test_login_success它也能按预期通过。如果用例之间互相依赖比如A用例创建的数据B用例要用那你跑全量的时候没问题但一旦断点调试或者指定运行某条用例就会莫名其妙挂掉排查成本极高。这个习惯一旦养成你的测试用例可维护性会上一个台阶。第二在操作前想清楚“这条用例的核心断言到底是什么”。UI自动化非常容易写成“点了这个按钮再点那个按钮”最后什么都没验证。好的用例一定要有一个明确的断言——要么是页面跳转了要么是弹窗提示出现了要么是数据发生了变化。没有断言的用例等于白跑。第三高频操作务必封装封装接口的统一返回值很重要。拿点击登录按钮来说封装方法的返回值最好设计成“登录结果页的对象”而不是“None”。这样用例写起来会非常舒服也是往Page Object方向自然演化的路径。第四调试阶段用有头模式定期跑一次无头模式。有头模式方便你直观地看到浏览器在做什么定位问题非常高效但发布到CI之前一定至少要跑一轮无头模式确保两种模式下行为一致。做Web自动化测试真正决定你效率上限的从来不是会不会调用Selenium的API而是你能不能稳定地处理等待、定位、异常这三件事。Selenium的API文档很薄但它在真实页面上的表现会把你对Web前端的理解、对浏览器原理的认知、对异常处理的敏感度全都逼出来。这篇文章里的每一个坑都是我拿实际项目时间换来的希望能帮你少走几步弯路。