资讯详情

资讯详情

Selenium Web自动化测试实战:从环境搭建到pytest工程化

1. Web自动化测试选型为什么我坚持用Selenium1.1 新工具层出不穷Selenium依然是很多团队的第一选择我做过不少Web自动化测试项目每次和别人聊到自动化测试选型总有人问我现在Playwright和Cypress这么火怎么还在用Selenium说实话Selenium确实年纪不小了2004年就诞生了但这二十年里它沉淀下来的东西不是随便一个网红工具就能替代的。大部分企业的核心业务系统、老旧的内部平台、复杂的跨域登录流程都有Selenium的影子。如果你掌握了Selenium再去学Playwright、Cypress会发现很多概念都是相通的——定位器、等待、页面对象模型这些思想最早都是被Selenium普及开来的。另外一点很实际Selenium支持的语言最全Java、Python、C#、Ruby、JavaScript都有官方绑定。我的团队里有写Python的也有写Java的用Selenium一套API可以保证大家的知识库互通。而Cypress主要面向JavaScriptPlaywright虽然现在支持多语言但生态和资料量还是没法和Selenium比。对于刚入门自动化测试的人来说Selenium是绕不开的基础课。1.2 Selenium的适用场景和它的短板Selenium最适合做端到端测试尤其是针对真实浏览器环境的回归验证。它可以模拟用户在Chrome、Firefox、Edge上的真实操作也能通过Selenium Grid做分布式并发。我以前做过一个电商后台的自动化项目有600多条用例就是用Selenium Grid塞到四台机器上跑二十几分钟跑完一轮效率完全能接受。但也要实话实说Selenium有几个天生短板。第一它对浏览器内部状态的控制能力弱比如要监听网络请求、模拟网络抖动这些Selenium做起来很别扭。第二它的执行速度不如Playwright快因为它是基于WebDriver协议每次命令都要经过一个中间层。第三它的移动端支持不够好虽然可以用Appium桥接但生态比较绕。所以如果你的项目是重前端交互的SPA并且团队全是JavaScript工程师那可以考虑Playwright。但如果你需要一个稳定、成熟、团队里人人都能上手的技术栈Selenium依然很值得优先考虑。2. 环境准备Python、Selenium和浏览器驱动一个都不能少2.1 搞定Python环境和工作目录先说环境搭建。Python版本建议直接用3.8以上我一般推荐3.10或3.11这两个版本兼容性最稳。安装的时候要注意勾选“Add Python to PATH”不然后面命令行里敲python会提示找不到命令。装完后在终端输一下python --version能正常显示版本号就说明没问题。接着创建一个专门的项目目录比如web_auto_test然后在这个目录下用venv建虚拟环境。很多新手图省事直接全局安装Selenium结果后面项目多了依赖版本互相打架非常痛苦。虚拟环境很简单mkdir web_auto_test cd web_auto_test python -m venv venv # Windows下激活 venv\Scripts\activate # Linux/Mac下激活 source venv/bin/activate激活成功后命令行前面会出现(venv)字样这样再安装依赖就不怕污染系统环境了。2.2 安装Selenium和版本对应的浏览器驱动接着安装Selenium库pip install selenium装上之后关键一步来了必须下载与浏览器版本匹配的WebDriver。很多刚接触Selenium的人在这步被卡住报了各种“SessionNotCreatedException”错误原因基本都是驱动版本和浏览器版本对不上。以Chrome为例先打开Chrome的“关于Chrome”查一下大版本号比如现在主流版本是122、123、124这些。然后到ChromeDriver下载页面https://googlechromelabs.github.io/chrome-for-testing/找到对应版本。下载后一定要把驱动文件放到PATH目录下或者放在项目里用webdriver.Chrome(executable_path...)指定路径。不过新版Selenium4.6以上有个好用的功能叫Selenium Manager它会在你首次调用webdriver.Chrome()的时候自动去下载匹配的驱动想省事的话可以依赖这个机制。如果你的浏览器是Firefox对应的驱动叫geckodriver如果是Edge就叫msedgedriver。原理都差不多核心就是版本必须匹配。2.3 写一个最小脚本验证环境环境搭好没搭好跑一下这个最简脚本就知道from selenium import webdriver from selenium.webdriver.chrome.options import Options # 无头模式不弹出浏览器窗口适合CI环境 opts Options() opts.add_argument(--headlessnew) opts.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsopts) driver.get(https://www.baidu.com) print(页面标题:, driver.title) driver.quit()我习惯先加上无头模式因为开发调试时可以不开浏览器窗口直接验证定位逻辑是不是有效。如果打印出了“百度一下你就知道”之类的标题说明Selenium安装成功浏览器驱动也完全正常。提示如果之前装过老版本的Selenium建议直接用最新版。Selenium 3和4的API有一些差异比如4.0把find_element_by_name这些方法废弃了统一用driver.find_element(By.NAME, xxx)网上很多老教程已经过时踩坑的人特别多。3. 元素定位的实战选择不要一上来就甩XPath3.1 八种定位方式实际项目里我主要用这几种Selenium定位元素的方式一共有8种ID、Name、Class Name、Tag Name、Link Text、Partial Link Text、CSS Selector、XPath。但是在实际项目里真正高频使用的其实就四个ID、CSS Selector、XPath以及偶尔会用到的Link Text。每当测试用例失败十次里有八次是元素定位失败。我总结的定位优先级是能用ID直接用ID因为ID在页面里理论上唯一定位速度也最快。ID不好用的情况下看看有没有稳定的CSS类名或name属性。再不行才用CSS Selector或XPath做更精细的定位。from selenium.webdriver.common.by import By # 按ID driver.find_element(By.ID, login-btn) # 按CSS选择器 driver.find_element(By.CSS_SELECTOR, .nav-item.active a) # 按XPath driver.find_element(By.XPATH, //div[classlogin-box]//button[text()登录])很多新手一上来就生成一段很长的XPath比如/html/body/div[3]/div[2]/form/div/button这种路径一旦页面结构微调就挂了。所以能用相对XPath就不要用绝对路径。3.2 XPath和CSS Selector怎么选我自己的习惯是能写CSS就优先写CSS因为CSS Selector语法简单、可读性好、速度也快。常见的CSS写法包括#id表示id.class表示classinput[namekeyword]表示属性匹配div p表示子元素但是有一些场景必须用XPath。比如要根据文本去找按钮“切换到标签页”这种中文文字CSS无法直接匹配文本而XPath可以driver.find_element(By.XPATH, //button[contains(text(), 立即购买)])另外XPath还支持各种轴、正则匹配、位置选择功能性比CSS强很多。比如找一个表单里的第二个输入框XPath可以用(//input[typetext])[2]。所以我的原则是能用CSS就用CSS需要按文本、按复杂层级找元素时就上XPath两者结合着用。3.3 动态元素的定位策略现在的前端大量使用JavaScript动态渲染很多元素在页面加载完之前根本不存在。常见的做法是把期望出现的元素交给等待机制后面会讲等它出现了再去定位。但还有一类更麻烦的动态元素比如每次刷新后id会带随机数或者class名会变比如news-item-12345、news-item-67890。这时候定位策略要改成“找稳定特征”。我一般会先看元素的父节点或者兄弟节点有没有稳定属性或者用XPath的starts-with、contains做前缀匹配driver.find_element(By.XPATH, //div[starts-with(id, news-item-)]//span[classtitle])还有更复杂的情况比如元素可能在iframe里或是在Shadow DOM下面。遇到这些情况如果直接定位会报NoSuchElement。这时候需要先切换进iframe或者用JavaScript去获取Shadow宿主节点。这部分我放到后面踩坑实战里细说。4. 等待机制把time.sleep换成显式等待降低95%的测试不稳定4.1 隐式等待和显式等待的本质区别很多刚学Selenium的人喜欢在代码里写time.sleep(2)意思是睡2秒再执行下一步。这确实简单粗暴但问题也很明显如果页面慢的时候2秒不够脚本就挂如果页面快的时候1秒就加载完了剩下的1秒纯属浪费时间测试总时长会被拉得很长。Selenium官方提供两种等待机制隐式等待implicitly wait和显式等待explicit wait。先看隐式等待它其实是一个全局超时设置告诉WebDriver在寻找元素时最多等多久driver.implicitly_wait(10) # 在这之后每次find_element找不到元素时都会轮询等待直到10秒上限隐式等待的问题是它只能处理“元素存在”这种情况但如果元素存在但处于不可点击、不可见状态它照样会报错。而显式等待就灵活得多它可以等待元素可点击、可见、文本变化等等。4.2 Expected Conditions最常用的显式等待写法显式等待的经典写法是WebDriverWait配合expected_conditions比如等待某个按钮出现并且可点击from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待最多10秒直到元素可见并且可点击 btn WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, login-btn)) ) btn.click()这套写法会比sleep稳定太多。until会每500毫秒检查一次条件一旦满足就立刻返回时间不会多浪费一秒。常用的Expected Conditions还有presence_of_element_located元素出现在DOM中不一定可见visibility_of_element_located元素可见text_to_be_present_in_element元素中包含指定文本frame_to_be_available_and_switch_to_it切换到iframe我建议所有涉及前端交互的代码都优先使用显式等待。这是提升脚本稳定性最便宜有效的手段。4.3 自定义等待条件有些项目里会遇到比较特殊的场景比如页面上有一个“加载中”的遮罩层遮罩存在期间后面的操作没法点击。标准条件里没有现成的“等到某个元素消失”这时候可以写一个自定义等待条件from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By # 等待loading遮罩消失 WebDriverWait(driver, 15).until( lambda d: len(d.find_elements(By.CLASS_NAME, loading-mask)) 0 )用lambda表达式的好处是简洁。如果逻辑复杂也可以定义一个函数返回布尔值。自定义等待条件在真实项目里非常有用比如等待某个接口返回后页面出现特定文本或者某个区块出现预期数量的子元素。掌握这个技能几乎能解决90%的动态等待问题。5. 一个完整的登录测试案例从打开浏览器到断言结果5.1 场景设定与基础代码框架纸上谈兵了这么多我们来实打实写一个登录测试用例。假设我们要测一个后台管理系统的登录页预期的流程是打开登录页输入用户名和密码点击登录按钮等待页面跳转后断言右上角出现“欢迎admin”字样。如果登录失败页面会有一个红色的错误提示框。直接看完整代码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 driver webdriver.Chrome() driver.maximize_window() driver.get(https://example.com/admin/login) wait WebDriverWait(driver, 10) try: # 定位输入框并输入内容 username wait.until(EC.presence_of_element_located((By.NAME, username))) username.clear() username.send_keys(admin) password driver.find_element(By.NAME, password) password.clear() password.send_keys(my_password) # 点击登录 login_btn wait.until(EC.element_to_be_clickable((By.ID, login-btn))) login_btn.click() # 等待登录成功后的欢迎文本出现 welcome wait.until( EC.text_to_be_present_in_element((By.CSS_SELECTOR, .user-info), 欢迎admin) ) assert welcome is True, 登录成功后未找到欢迎文本 print(登录测试通过) except Exception as e: # 失败截图留档排查 driver.save_screenshot(login_failed.png) raise e finally: driver.quit()这个流程看起来很简短但有几处细节很关键。第一输入框最好先clear()再send_keys()避免输入框默认值干扰。第二点击之前一定要等待元素可点击而不是出现后立刻点。第三断言一定要等待预期文本出现而不是直接获取text然后立刻比对因为页面跳转需要时间过早获取会拿到空字符串。5.2 处理下拉框、复选框、弹窗和文件上传登录测试只是入门真实项目的页面元素更杂。这里我把高频的交互操作挨个写一遍都是可以直接抄走的。下拉框select处理时Selenium有专门的选择器from selenium.webdriver.support.ui import Select select_el driver.find_element(By.ID, city) select Select(select_el) # 三种选择方式 select.select_by_value(beijing) # 根据value属性 select.select_by_visible_text(北京) # 根据显示的文本 select.select_by_index(2) # 根据下拉索引从0开始复选框比较简单checkbox driver.find_element(By.NAME, agree) if not checkbox.is_selected(): checkbox.click()弹窗分两种。一种是浏览器的原生alert这种直接切换过去alert driver.switch_to.alert print(alert.text) # 弹窗文本 alert.accept() # 点确定 # alert.dismiss() # 点取消另一种是非原生的自定义模态框这种本质上还是页面元素用正常的定位方式操作即可但要注意点击模态框里按钮前先等模态框渲染出来。文件上传是比较容易踩坑的很多类型文件上传的input元素是typefile这时候不要对input模拟点击然后等弹窗而是直接用send_keys()传文件路径upload_input driver.find_element(By.CSS_SELECTOR, input[typefile]) upload_input.send_keys(/home/user/测试数据.xlsx)这样不用处理操作系统的文件选择窗口是最稳的做法。5.3 断言失败时的现场截图测试跑在无人值守的环境里一旦失败最痛苦的就是不知道现场长什么样。所以我的每个关键测试步骤都会加截图机制尤其是在断言失败或者异常抛出的时候。一个比较实用的做法是封装一个take_screenshot函数import os from datetime import datetime def take_screenshot(driver, namescreenshot): if not os.path.exists(screenshots): os.makedirs(screenshots) timestamp datetime.now().strftime(%Y%m%d_%H%M%S) path fscreenshots/{name}_{timestamp}.png driver.save_screenshot(path) print(f截图已保存: {path})截图看着简单但真的到排查问题的时候一张现场截图能准确告诉你页面卡在哪一步、弹出什么错误提示、布局是不是乱了比看十万行日志都管用。我现在写自动化用例凡是比较关键的断言前面都会放一个screenshot调用确保失败时留底。6. 用pytest把Selenium脚本变成可维护的测试工程6.1 为什么不用unittest而选pytestSelenium本身只是一个浏览器自动化库它不管你怎么组织测试用例、跑测试报告。所以我们需要借助测试框架。Python自带的unittest也能用但它的断言方式、用例规范、参数扩展都偏古板。我用了几年pytest后现在新项目一律用pytest。pytest的核心好处是上手简单写起来简洁不需要像unittest那样定义类和方法。比如这个最朴素的天使用例def test_login(): # 假设已经创建好driver driver.get(https://example.com/login) assert 登录 in driver.title但实际项目里我们还要兼顾webdriver的创建和销毁这才是pytest能发力的地方。6.2 用fixture统一管理浏览器实例fixture是pytest的一大神器。我可以定义一个会话级的fixture让整个测试过程只启动一次浏览器所有测试用例共享这个driver。也可以定义一个函数级的fixture每个用例都开新浏览器更隔离但会更慢。我的习惯是分两层。一层是会话级别的driver另一层是每用例之前的登录状态import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options pytest.fixture(scopesession) def driver(): opts Options() opts.add_argument(--headlessnew) opts.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsopts) yield driver driver.quit() pytest.fixture() def logged_in_driver(driver): # 这里可以写登录流程返回已登录的driver driver.get(https://example.com/login) # ... 登录操作 yield driver如果测试用例比较多共享一个浏览器能省很多时间但要注意用例之间如果互相影响就必须考虑隔离。我通常会把只读类的用例放共享浏览器里涉及修改数据的用例单独开新会话。6.3 参数化测试和数据驱动登录测试经常需要验证多组账号数据比如正确的账号能登录错误密码会提示错误账号不存在会提示账号不存在。如果用pytest的参数化代码量能省一大半import pytest pytest.mark.parametrize(username, password, expected, [ (admin, correct_pass, 欢迎admin), (admin, wrong_pass, 密码错误), (nobody, anything, 账号不存在), ]) def test_login_scenarios(driver, username, password, expected): driver.get(https://example.com/login) driver.find_element(By.NAME, username).send_keys(username) driver.find_element(By.NAME, password).send_keys(password) driver.find_element(By.ID, login-btn).click() assert wait_for_text(driver, expected), f预期出现文本: {expected}注意参数化多条数据执行时如果前面用例登录成功后面用例可能需要清理会话否则记住登录态影响后续。这里可以设计每个参数用例对应不同的用户类型或者在测试开始时清一下Cookiedriver.delete_all_cookies() driver.refresh()这条虽然看着土但能避免很多数据串扰问题。6.4 生成测试报告和集成到CIpytest可以生成非常直观的测试报告。最简单的方法是安装pytest-html插件pip install pytest-html运行pytest test_login.py --htmlreport.html --self-contained-html在CI流水线比如Jenkins或者GitLab CI里我一般还会加上-x失败即停或者--maxfail5来控制失败速度。如果用例跑得比较久还可以用pytest-xdist插件并行执行把多个测试文件分到不同进程里跑pytest -n 4 --htmlreport.html --self-contained-html四进程并行之后原本半小时的用例集可能压缩到十分钟左右收益非常明显。不过并行执行时要注意driver不能共享fixture需要改成进程级独立实例否则互相抢资源会莫名其妙报错。7. 那些年我踩过的坑iframe、遮挡元素、驱动版本7.1 明明定位到了却一直提示元素不可交互这类问题在真实项目里出现频率极高。页面代码里确实有这个元素find_element也能找到但点击的时候报ElementClickInterceptedException。排查路径通常是这样第一步看看这个元素是不是被其他元素覆盖了。比如弹窗遮罩、悬浮广告、Cookie协议条它们可能盖住了要点的按钮。解决方案是等待遮挡元素消失或者用JavaScript绕过遮挡直接点击element wait.until(EC.presence_of_element_located((By.ID, submit-btn))) # 直接强制点击 driver.execute_script(arguments[0].click();, element)但要注意强制点击是“绕道”如果元素本身被禁用那即使强制点击也不会有反应。先判断元素是否is_enabled()再决定要不要特殊处理。第二种情况是元素处于滚动区域之外被盖住了。这时候要先滚动到元素位置from selenium.webdriver.common.action_chains import ActionChains actions ActionChains(driver) actions.move_to_element(element).perform() element.click()第三种情况是元素在iframe里。很多人忽略了这个因为直接在页面源码里看不到iframe里的元素。遇到这种必须先用switch_to.frame()切进去等处理完再切回来# 通过索引切换或者用name/id driver.switch_to.frame(0) driver.find_element(By.ID, inner-btn).click() # 切换回默认主文档 driver.switch_to.default_content()如果iframe本身带有动态id就用WebDriverWait等iframe可切换from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) wait.until(EC.frame_to_be_available_and_switch_to_it((By.XPATH, //iframe[contains(src,plugin)])))这样处理之后后续定位就会落在iframe内部的文档里。切记操作完后要用driver.switch_to.default_content()切回主文档否则后面的元素定位会全部失败。7.2 驱动版本不匹配的“灵异事件”Selenium环境问题里驱动版本不匹配是最折磨人的。经常遇到开发同事说“我昨天还能跑今天突然报错了”一看错误信息是SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 114这大概率是浏览器自动更新了但驱动没有跟着更新。特别是Chrome这类默认开启自动更新的浏览器睡一觉起来版本就变了。解决方案主要有两种。第一种是锁浏览器版本关闭自动更新在团队内部统一分发安装包。第二种是每次跑测试前用脚本检测浏览器版本自动去下载匹配的驱动。我比较推荐第二种因为更省心。用webdriver_manager库可以做到自动管理pip install webdriver-manager然后这样用from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice)webdriver-manager会在本地缓存驱动当浏览器版本变化时自动下载对应版本从此告别“版本不匹配”的幺蛾子。7.3 Shadow DOM里的元素怎么定位现在很多前端组件库比如一些web component会把内部结构封装在Shadow DOM里用普通的方式点击元素Selenium会直接告诉你找不到。为什么因为Shadow DOM相当于一个隔离的私有边界DOM树和主文档是独立的。不过我们仍然有办法操作。先通过JavaScript拿到shadow root节点再进一步查找里面的子节点def shadow_root_click(driver, host_selector, inner_selector): script const host document.querySelector(arguments[0]); const shadow host.shadowRoot; const target shadow.querySelector(arguments[1]); return target; target driver.execute_script(script, host_selector, inner_selector) driver.execute_script(arguments[0].click();, target)这个方法能解决大部分shadow DOM场景。不过要慎重如果元素在多层Shadow DOM嵌套里脚本还得递归。目前Selenium提供了shadow_root的API接口部分情况下也可以用element driver.find_element(By.TAG_NAME, my-component) shadow element.shadow_root inner_btn shadow.find_element(By.CSS_SELECTOR, .inner-btn) inner_btn.click()Shadow DOM处理比较烦但掌握之后也不难。测试框架能不能做到对页面结构不敏感很大程度拼的就是这些边角场景的处理能力。7.4 测试慢、不稳定怎么提速和定位问题如果测试用例数量上去了最让人头疼的就是两点跑得慢还有不稳定。我踩过不少坑后总结出几个实在的优化方向。第一个是减少不必要的页面跳转。当我有多条用例都要从同一个页面开始操作时不如让driver停留在那个页面用例之间复用状态。如果担心状态污染可以每条用例之前重置关键数据而不是每次重新走一遍完整流程。第二个是慎用无头模式。无头模式虽然不显示浏览器窗口节省资源但某些奇特页面在无头模式下渲染结果和正常模式不一样。我一般在本地调试时用--headlessfalse只有在CI里才开无头。如果遇到无头模式偶发失败、非无头模式稳定通过的情况优先检查是不是页面有动画、懒加载或者等待条件不够精确。第三个是重视等待条件的粒度。不能用统一的time.sleep(3)硬等而是要因地制宜用显式等待。每条用例等待时间越精准总耗时越短稳定性也越高。我甚至会把等待时间设成枚举值比如“短等2秒”“中等5秒”“长等15秒”让用例可读性更好。第四个是善用日志。给每一步操作加上带时间戳的日志比如“点击了登录按钮”“跳转到订单页”一旦用例失败日志能清晰还原操作路径省去反复猜过程的时间import logging logging.basicConfig(levellogging.INFO, format%(asctime)s %(levelname)s %(message)s) logging.info(开始点击登录按钮) login_btn.click() logging.info(点击完成等待跳转)这四个方面一个一个排查绝大多数“时好时坏”的测试都能稳定下来。真正跑过上千条用例之后你才会意识到稳定比速度更重要但优化得当的话二者可以兼得。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →