资讯动态

Python+Selenium Web自动化测试实战:从环境搭建到全链路脚本

发布时间:2026/10/14 11:11:45 来源:尧图企业网站定制
做了几年Web测试有一段时间我几乎把全部精力都花在手工点页面上。每天重复登录、填表单、验证数据的流程明明逻辑没变却要一遍遍在浏览器里点来点去。直到我认真把Python和Selenium这套组合用起来才体会到什么叫“写一次脚本让浏览器自己去跑”。这篇文章就是基于我自己从零搭建Web自动化测试的真实经验从方案选型、环境搭建、脚本编写到问题排查梳理出一套可以直接参考的完整路径。无论你是刚接触自动化测试的脚本新手还是在手工测试里挣扎想转型的从业者这套思路和代码都能帮你少走不少弯路。1. 项目整体思路与方案选型1.1 我们到底要解决什么问题先聊清楚一个事自动化Web测试不是把手工点的每一步机械地录下来而是通过代码直接驱动浏览器执行一系列操作再断言页面结果是否符合预期。它的核心价值在于“可重复”和“回归”。我自己经历过的场景很典型一个电商系统每两周发一个版本每次发版前都要把登录、商品搜索、加购物车、下单支付这个主链路完整走一遍。手工测一次半小时全量回归可能要一整天。后来我把主链路写成了自动化脚本每次发版前跑一遍只需要几分钟冒烟测试从半天压缩到半小时以内。这才是自动化测试存在的意义——让机器去替人做那些重复、确定、枯燥的验证动作人把精力留给探索性测试和复杂业务逻辑。如果你处在这种状态项目频繁迭代、核心流程稳定、页面元素大多有稳定标识那用Selenium做Web自动化测试就是很合理的选择。它不是万能药但覆盖大部分前端功能回归绰绰有余。1.2 为什么偏偏选Selenium而不是其他方案选工具这件事我踩过不少坑。最早试用过商业化工具录制回放确实炫但脚本一复杂就失控维护成本高得离谱。后来转向开源方案在Selenium、Playwright、Puppeteer之间犹豫了很久。最终定下来Selenium核心原因有三点第一语言生态。Selenium支持Python、Java、C#、Ruby等主流语言而Python在测试领域的简洁性毋庸置疑。我可以用最少的代码写最清晰的用例团队其他成员上手也快。第二浏览器兼容性。Selenium WebDriver是W3C标准Chrome、Firefox、Edge、Safari都原生支持。很多公司的用户群就是用不同浏览器访问系统的只有Selenium能一套用例多浏览器跑这种覆盖面是其他工具很难比的。第三社区积累。Selenium火了十多年遇到任何奇怪问题几乎都能在社区里找到答案。我后来遇到的一个动态元素定位问题就是在老帖子里找到的思路。选测试工具不是选最潮的是选最不容易让你卡死的。1.3 整套方案的架构长什么样我习惯把自动化测试项目分成三层这个分层思路也是业界常见的做法底层是驱动层包含浏览器驱动程序负责把脚本指令转换成浏览器能识别的原生操作中间是测试逻辑层用Page Object模式把页面元素和操作封装成独立模块测试用例不直接接触繁杂的选择器上层是用例层只描述业务动作和预期结果。这样分层的好处非常直接页面改了只需要改对应Page Object里的定位逻辑用例代码基本不用动。后来产品经理要求把登录按钮的CSS类名换掉我只改了一个文件里的一行定位代码五十多条用例全部照常跑过。这个体验让我彻底相信了分层设计的价值。2. 环境搭建与Selenium核心机制2.1 从安装依赖到浏览器驱动配置写Selenium脚本前先把基础环境配好。我用的是Python 3.9配合Selenium 4.x版本整体步骤很简单pip install selenium光装这个包还不够。Selenium本身不操作浏览器它要借助WebDriver来指挥浏览器。以Chrome为例需要下载和浏览器版本匹配的chromedriver。以前还要手动下载驱动再配置PathSelenium 4.13之后有了Selenium Manager可以自动管理驱动版本省了好多事。但我还是建议你至少知道手动配驱动的路径因为公司内网环境经常没有外网访问权限。我当时在公司内网就踩过这个坑。首台测试服务器没法访问外网pip的镜像源配了内网地址才装上Selenium但chromedriver下载这一步卡了很久。最后是在有外网的机器上下载驱动压缩包拷到内网后解压再把驱动路径通过webdriver.Chrome(executable_path/path/to/chromedriver)指过去才解决。注意你用的是Chrome还是Edge驱动必须严格对应浏览器。版本号差太远会直接抛session not created异常这个排查起来很浪费时间。2.2 浏览器实例启动与参数配置启动一个浏览器实例最基础的写法是from selenium import webdriver driver webdriver.Chrome() driver.get(https://example.com)但实际工作中我几乎不会用这么裸的配置。跑自动化测试时通常不希望弹真实窗口尤其是一跑几十个用例时弹窗满屏飘机器会卡到怀疑人生。所以无头模式是必开的from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) # Linux服务器上常需要这个 options.add_argument(--disable-dev-shm-usage) driver webdriver.Chrome(optionsoptions)这里有个细节值得展开--headlessnew是新版无头模式对WebGL、CSS渲染的支持比旧版好。如果你在无头模式下截图发现某些元素渲染异常可以试试加--window-size1920,1080很多布局问题其实就是默认窗口太小导致的。我之前在一个后台管理系统的测试中就遇到过表格列重叠的问题当时百思不得其解代码在本地有头模式跑得好好的。后来排查到是服务器的虚拟显示分辨率只有800x600页面按响应式布局收缩后表格全挤成一团。加了窗口尺寸参数后问题直接消失。2.3 元素定位几大策略对比与我的取舍驱动浏览器打开页面后一切操作的本质都是先找到元素再对元素做操作。Selenium提供了多种定位策略我按实际使用频率给你排个序ID最高优先HTML里ID通常唯一定位速度快但页面里不是所有元素都有IDCSS Selector灵活度极高支持层级、属性、伪类我日常工作里用这个最多XPath能处理CSS不好表达的结构比如根据文本内容定位、找父级兄弟节点但性能略差写长了也难维护链接文本只适合精确链接定位局限性强Name表单元素常见但页面里重复Name不少要注意我踩过最大的坑是过度依赖XPath。早期写脚本时不管什么元素都抄浏览器开发者工具拷一个XPath出来后来页面结构一改脚本就大面积报错。现在我给自己定的规矩是能用CSS Selector就用CSS SelectorXPath只在确实需要处理复杂DOM关系时才用而且尽量用相对路径而不是从html根节点一路写到底。比如定位一个按钮我推荐写成driver.find_element(By.CSS_SELECTOR, button.submit-btn)而不是一大串/html/body/div[2]/div[3]/form/button。前者对DOM结构变化的容忍度高得多。2.4 显式等待与隐式等待防元素未加载的关键新手最容易犯的错是脚本跑得飞快页面没跟上元素还没渲染出来就直接去操作结果抛出NoSuchElementException。这本质上是同步问题。Selenium给了两种等待方案隐式等待是设置一个全局超时时间每个查找元素操作都会轮询等待显式等待则是针对特定条件等待某个元素出现、可点击、可见以后再继续执行。我个人的经验是不要用隐式等待做主要防御手段。隐式等待一旦设置太长会让定位失败的元素白白等很久拖慢整个测试套件。显式等待更精准代码可读性也更好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) submit_btn wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, button.submit-btn)) ) submit_btn.click()用expected_conditions判断元素可点击比time.sleep(3)这种固定等待靠谱太多。固定等待最大的问题在于不确定性网络稍慢就报错网络快又白白多等。后来我把脚本里的sleep全部改成显式等待稳定性直接上了个台阶。3. 实操过程从登录到订单全链路脚本3.1 一个刚需场景登录用例怎么写理论讲再多不如来一段实际代码。我拿一个模拟项目X来举例——一个典型的电商系统我需要测试用户从登录到提交订单的完整链路。先写登录用例的骨架import time 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 def test_login(): driver webdriver.Chrome() wait WebDriverWait(driver, 10) try: driver.get(https://mall.example.com/login) # 填写用户名和密码 username wait.until(EC.presence_of_element_located((By.ID, username))) username.send_keys(qa_user) password driver.find_element(By.CSS_SELECTOR, input[namepassword]) password.send_keys(Passw0rd!2025) # 点击登录按钮 login_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, button.login-btn))) login_btn.click() # 断言跳转后的页面包含用户名 welcome WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, .user-info)) ) assert qa_user in welcome.text print(登录成功断言通过) finally: driver.quit()这里有个细节登录按钮点击后页面可能重新加载或者走Ajax跳转断言前最好用WebDriverWait等待目标元素出现。我遇到过不少脚本跑到这里就断言失败原因就是刚点完登录按钮立刻去查元素页面还停留在登录态。3.2 复杂点的场景商品搜索到加购再到提交订单单测登录只能算入门真实业务链路长得多。我把搜索商品、选规格、加入购物车、提交订单整合成一个用例片段你可以直接参考from selenium.webdriver.common.keys import Keys # 搜索商品 search_input wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, input.search-box))) search_input.send_keys(无线机械键盘) search_input.send_keys(Keys.ENTER) # 等待搜索结果中出现商品卡片并点击第一个商品 first_item wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, .goods-item .goods-title)) ) first_item.click() # 切换窗口因为商品详情通常在新标签页打开 handles driver.window_handles driver.switch_to.window(handles[-1]) # 选择规格并加购 spec_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, .sku-item.active))) spec_btn.click() add_cart_btn driver.find_element(By.CSS_SELECTOR, button.add-cart-btn) add_cart_btn.click() # 去购物车结算 cart_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, .cart-icon))) cart_btn.click() checkout_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, button.checkout-btn))) checkout_btn.click() # 在订单确认页提交订单 submit_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, button.submit-order))) submit_btn.click() # 断言订单编号生成 order_no wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .order-no))) assert order_no.text.startswith(ORD)这段代码里有几个值得注意的点都是我实际踩出来的经验。第一driver.switch_to.window(handles[-1])这一步很容易漏。点击商品后如果在新标签页打开不切换窗口的话脚本会在原页面上找不存在的元素。更稳的做法是先在switch_to.window前判断窗口数量是否增加避免网络慢导致窗口还没创建就去切换。第二spec按钮的定位。很多商品的规格选项是动态渲染的直接取active类可能还没生成。我做的最简陋的兜底方案是加显式等待更优雅的方案是遍历所有sku-item选择第一个可点击的from selenium.webdriver.remote.webelement import WebElement def pick_first_clickable_sku(driver, wait): skus wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, .sku-item)) ) for sku in skus: if sku.is_enabled() and sku.is_displayed(): sku.click() return3.3 参数化数据驱动把登录用例拆成多条数据真实测试不能只测一条正确路径。登录功能至少要考虑账号错误、密码错误、空表单、手机号格式不对。如果每条用例都重写一遍脚本维护成本太高。我们可以把数据和逻辑拆开。我的做法是创建一个简单的数据列表用循环驱动同一个登录函数login_cases [ {username: qa_user, password: Passw0rd!2025, expect: success}, {username: wrong_user, password: Passw0rd!2025, expect: fail}, {username: qa_user, password: WrongPass, expect: fail}, {username: , password: , expect: fail}, ] for case in login_cases: result login_and_check(case[username], case[password]) if case[expect] success: assert result is True else: assert result is False这个模式下新增一条测试数据只需要往列表里加一个字典完全不用动核心函数。后面如果你想把数据放到Excel、YAML甚至数据库里也只是换个数据源的事逻辑层完全不受影响。3.4 测试报告与日志给脚本配上一双眼睛脚本能跑还不够你得知道它跑得怎么样。最简单的输出方式就是打印日志但真实项目里我建议直接上日志模块import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, filenametest_run.log ) logger logging.getLogger(__name__) logger.info(开始执行登录用例)配合截图更是排查问题的利器。Selenium提供了获取页面截图的API我通常会在断言失败时截一张图import os def take_screenshot(driver, name): ts time.strftime(%Y%m%d_%H%M%S) path os.path.join(screenshots, f{name}_{ts}.png) driver.save_screenshot(path) logger.info(f截图保存至: {path})这样做的好处是脚本半夜定时跑失败了第二天你打开test_run.log和截图目录能直接还原现场是在哪一步、什么页面状态下出的问题。省去了大量“重跑一遍看能不能复现”的时间。4. 常见问题排查与避坑技巧4.1 元素定位失败的几种典型情形NoSuchElementException是Selenium世界里的第一大异常出现频率极高。我梳理了几种引发这个异常的情形页面还没加载完就去找元素。解决办法是加WebDriverWait优先级最高的是用presence_of_element_located或者visibility_of_element_located判断。页面在frame或shadow DOM中默认的查找上下文覆盖不到。我遇到过一个支付页面支付按钮嵌在iframe里直接find_element怎么都找不到切进iframe以后一次就成功了driver.switch_to.frame(paymentIframe) pay_btn wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, button.pay-btn)))元素在页面底部没有被浏览器视口加载。这时候需要先滚动到元素位置再操作driver.execute_script(arguments[0].scrollIntoView(true);, element)4.2 StaleElementReferenceException动态页面的头号敌人这个异常名翻译过来叫“元素引用过期”。意思是元素对象还在但它挂载的DOM节点已经被更新或者重新渲染再操作旧引用就会报错。我印象最深的是分页表格的测试。每次点击下一页整个表格的DOM都会被重建我之前拿到的元素引用全部失效。解决办法也很直接每次点击翻页后重新获取元素# 错误示范先找到元素再点击翻页后直接操作 page_btn driver.find_element(By.CSS_SELECTOR, .next-page) page_btn.click() page_btn.click() # 这里大概率报 StaleElementReferenceException # 正确做法每次点击前重新定位 driver.find_element(By.CSS_SELECTOR, .next-page).click() wait.until(EC.staleness_of(page_btn)) driver.find_element(By.CSS_SELECTOR, .next-page).click()用staleness_of等待旧元素消失可以在页面上任何交互逻辑里当作同步点。4.3 元素不可交互被遮挡或者没到可点击状态有时候元素明明定位到了但click()就是报错提示element not interactive。我总结了下通常有两类原因一类是元素被其他元素遮挡。比如弹窗遮罩层没完全消失按钮被覆盖着。另一类是元素有disabled或readonly属性此时用element_to_be_clickable等待就会一直卡到超时。排查这类问题的标准思路是先看页面实时状态确认元素是可见可点击的再看脚本里的等待条件是否匹配。如果确实被遮挡优先上显式等待遮罩层消失mask driver.find_element(By.CSS_SELECTOR, .modal-mask.in) wait.until(EC.invisibility_of_element(mask))4.4 用例间相互影响导致串行问题写自动化测试很容易踩的一个坑是用例之间有隐性依赖。比如一个测试用例先执行登录另一个用例假设登录态已经存在直接去访问需要鉴权的页面。这样当第一个用例失败时后面所有用例跟着失败排查起来也特别痛苦。我的改进方案是让每个用例都自己负责前置条件。需要登录的用例开头就完成登录动作需要特定数据的用例先通过接口或者页面操作构造数据。这样每个用例可以独立运行不会因为执行顺序改变而崩掉。4.5 实践中的浏览器兼容性隐患测试脚本在你本地Chrome上跑得好好的不代表它能在其他浏览器里也跑得动。我后来用Selenium Grid把用例同时分发到Chrome和Edge环境执行就发现过不少兼容性差异有些元素的CSS类名在Edge里有大小写不同有些动画在Firefox里执行更慢显式等待超时时间要延长旧版IE模式下部分CSS选择器不生效如果你团队的用户确实跨浏览器我建议尽早把多浏览器执行纳入测试范围不要等上线前才临时查。5. 进阶实践Page Object模式与CI集成5.1 Page Object模式让测试代码真正可维护脚本越写越多以后如果你还到处写着driver.find_element(By.ID, xxx)那改版时你会非常痛苦。Page Object模式的核心思想是把页面抽象成一个类把页面里的元素定位和操作方法都封装在类里测试用例只调用类的方法。我以登录页面为例class LoginPage: def __init__(self, driver, wait): self.driver driver self.wait wait def load(self): self.driver.get(https://mall.example.com/login) return self def enter_username(self, username): el self.wait.until(EC.presence_of_element_located((By.ID, username))) el.clear() el.send_keys(username) return self def enter_password(self, password): el self.driver.find_element(By.CSS_SELECTOR, input[namepassword]) el.clear() el.send_keys(password) return self def click_login(self): btn self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, button.login-btn))) btn.click() return self测试用例就变得非常精简def test_login_success(): driver webdriver.Chrome() wait WebDriverWait(driver, 10) try: login_page LoginPage(driver, wait).load() login_page.enter_username(qa_user).enter_password(Passw0rd!2025).click_login() welcome wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .user-info))) assert qa_user in welcome.text finally: driver.quit()这样说吧Page Object模式最大的价值不是减少代码量而是让“页面变化影响测试脚本”这件事被限制在一个小范围内。登录页结构变了我只需要改LoginPage几十个依赖登录态的用例不会跟着改。5.2 无头模式与定时执行让回归测试自动跑起来把自动化测试跑起来只是第一步让它自动跑才是解放生产力的关键。我一般用两种方式实现定时执行第一种是用操作系统自带的任务计划程序比如Windows计划任务或者Linux的cron。写一个简单的shell脚本执行python test_login.py这类命令每天凌晨固定跑再把日志输出到文件里。这个方案零成本适合小团队。第二种是基于CI流水线。代码仓库里配置一个测试任务每次提交代码时触发测试测试失败就阻断合并。这个思路能尽早发现问题但前期需要投入搭建流水线的时间。我在模拟项目X里用的是第一种每天凌晨5点全量回归早上到办公室先看日志。如果一片绿基本可以安心做手工探索性测试如果红了打开截图和日志直接定位问题比手工回归高效太多。5.3 我踩过的一个无头模式特殊坑远程服务器上跑无头模式有个常见异常WebDriverException: unknown error: DevToolsActivePort刚开始会以为驱动没装好其实多数原因是Chrome在Linux环境缺少必要依赖包或者沙箱权限受限。解决方法是先确认系统里有没有Chrome本体Selenium只装了驱动是不够的。然后看驱动日志通常会提示缺哪些库libnss3、libatk这些基础依赖需要补齐。生产服务器的最小化安装经常不带这些。另外如果并发跑多个浏览器实例建议每个实例用不同的用户数据目录否则会资源冲突options.add_argument(--user-data-dir/tmp/chrome_test_profile_1)5.4 项目规模变大后pytest是你的好伙伴脚本一多靠if __name__ __main__逐个运行已经不够用了。我后期把项目切到了pytest框架配合fixture管理浏览器实例用例编写和结果收集都规整了不少。这里给一个小小的fixture示例import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options pytest.fixture def driver(): options Options() options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions) yield driver driver.quit() def test_login_with_fixture(driver): driver.get(https://mall.example.com/login) assert 登录 in driver.titlepytest的好处还在于天然支持参数化、用例分组、插件扩展以及生成JUnit XML格式测试报告。后面如果要对接CI系统这个格式是标准的很多平台上都能直接展示测试结果。可以说从零散脚本过渡到pytest是自动化测试项目从“能跑”走向“能用”的关键一步。6. 写在最后的几点体会从最早靠time.sleep硬等页面加载的新手到后来用Page Object、显式等待、无头模式、pytest编排整套回归体系我最大的感触是自动化测试的核心不是炫技而是稳定地省时间。保持脚本稳定的几个习惯值得你从第一天就养成选择稳定的定位策略能靠ID和CSS就不靠复杂的XPath等待条件要精准明确等什么元素出现而不是盲目睡几秒用例之间互相独立谁都不依赖谁的执行顺序失败时要有足够的现场证据日志、截图、控制台输出缺一不可。还有一个我后来才悟到的点自动化测试并不是完全替代手工测试。复杂页面的视觉效果、交互手感、边界逻辑这些机器很难验证全面。它更适合做回归保障把确定性的检查交给脚本把不确定性留给人的判断。你可以先把最核心的日常流程自动化比如登录、搜索、下单、支付这类的关键链路再从报障和需求变动最频繁的业务模块入手。用最小成本覆盖最大风险逐步再把它扩展到整个全流程。这套方法我在模拟项目X里落地之后团队的发版信心强了不少大家终于敢在改完代码之后快速验证老功能了。希望这篇文章的思路和代码也能让你少踩几个坑早一点把Web自动化测试跑起来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑