ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium显示等待(Explicit Wait)核心机制与工程实践详解

Selenium显示等待(Explicit Wait)核心机制与工程实践详解 做Selenium自动化测试最让人头疼的往往不是定位不到元素而是元素明明就在那里脚本却报超时。我刚入行那会儿遇到这类问题第一反应就是把sleep(3)改成sleep(5)结果照样间歇性失败。后来我才明白问题根本不在sleep的时间长短而在于我压根没搞懂页面元素加载的时序逻辑。这篇就来说清楚显示等待Explicit Wait——Selenium处理动态加载场景时最核心、也最值得花时间去掌握的等待手段。无论你是刚接触自动化的小白还是已经被各种偶发超时折磨的测试开发这篇文章都值得你耐心看完。1. 为什么等待是自动化测试里最容易踩的坑1.1 元素加载时序问题是怎么来的只要做Web UI自动化早晚会遇到一个尴尬场面脚本点击登录按钮结果控制台报ElementClickInterceptedException或者明明账号密码都填对了却因为页面还在loading断言直接失败。表面上看是脚本稳定性不行本质上是浏览器页面的加载顺序和Selenium的操作时序错位了。现代Web页面早就不是一锤子买卖的静态HTML。你打开一个页面浏览器先解析HTML骨架然后加载CSS、JS、图片JS可能还要发好几个Ajax请求拿到响应后再动态渲染出一批新节点。这个过程中存在大量的时间窗口某个元素可能已经在DOM里了但还没有渲染完成也可能渲染完了但还被遮罩层盖着甚至可能在你的find_element执行完到click()执行前这几十毫秒内被前端框架重新刷掉了一遍。Selenium的底层操作是一个非常机械的执行器它只管在你下达指令那一刻去找元素、去点击它不懂这个按钮要等接口返回后才能点亮这些业务逻辑。于是等待就成了自动化脚本里绕不开的一环。1.2 三种等待方式的对比与选型Selenium里常见的等待方式有三个强制等待time.sleep、隐式等待implicitly_wait、显示等待WebDriverWait。很多初学者一上来就三种乱用结果脚本跑得又慢又脆。我以一个实际项目为例说说三者的本质差异。强制等待最容易理解就是把线程挂起固定秒数。问题在于你根本无法预知页面到底几秒能好网络抖动一下3秒不够5秒又白白浪费。脚本是人等页面而不是页面就绪后人执行稳定性全靠运气。隐式等待是在整个driver生命周期内设置一个全局兜底值每次find_element时如果元素没出现轮询查找直到超时。它的优势是省事写一行全部生效但问题也很明显它只关心元素在不在DOM里不关心元素可不可见、可不可点击、有没有被遮挡。两种等待混用更是大坑我后面专门说。显示等待则完全不同。它针对的是某个指定元素在指定时间内满足指定条件这个特定诉求条件满足就立刻往后走条件不满足就继续轮询直到超时抛异常。它既有精确性又有灵活性是处理异步加载、复杂交互场景的首选方案。1.3 隐式等待和强制等待到底有什么坑先说隐式等待。driver.implicitly_wait(10)设置了10秒这10秒是每次find_element调用时都可能消耗的。想象一下你页面里有50个元素要逐一查找如果每个都等到超时脚本卡到怀疑人生。更麻烦的是隐式等待和显示等待会在Selenium内部产生竞争关系显示等待内部的轮询过程中如果调用了find_element它同时受隐式等待影响导致等待总时长可能超出你的预期。官方文档和Selenium维护者都反复提醒过尽量不要混合使用这两种方式。再说强制等待。time.sleep(5)这类代码最典型的问题就是不精确——页面快了它拖节奏页面慢了它救不了场。我见过有同事为了掩盖某个偶发问题在代码里一路sleep(3)、sleep(5)地堆下去整个测试集跑完要多花十几分钟稳定性却没有任何本质提升。踩过这些坑之后我现在的策略非常明确全局不设置隐式等待代码里不写无意义的强制等待所有跟等相关的逻辑全部收敛到显示等待里。这套打法在PC端Web、移动端H5、小程序WebView场景里都跑得很稳。2. 显示等待的核心机制与用法详解2.1 WebDriverWait和expected_conditions入门显示等待在Selenium里通过WebDriverWait实现配合expected_conditions通常简写为EC模块使用。核心逻辑你可以理解成一个带有超时机制的轮询循环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.get(https://example.com/login) wait WebDriverWait(driver, 10) login_button wait.until(EC.element_to_be_clickable((By.ID, login-btn))) login_button.click()这里的含义是最多等10秒每间隔默认0.5秒检查一次id为login-btn的元素是否可见且可点击一旦满足立即返回元素并继续执行10秒内一直不满足就抛出TimeoutException。这套机制比我前面说的sleep高级在哪儿它在条件满足的瞬间就放行不会多等一秒钟而且它把等到什么状态这个语义表达得非常清楚代码可读性直接提升一个档次。除了until还有一个反向方法until_not等待某个条件变为不成立。比如点击提交按钮后等待loading图标消失就可以写until_not(EC.visibility_of_element_located((By.CLASS_NAME, loading)))非常实用。2.2 高频使用的等待条件速查表expected_conditions模块里封装了二十多个现成的等待条件下面是我在实际项目中真正高频使用过的一些整理出来方便对照查阅。等待条件作用典型场景presence_of_element_located元素在DOM中出现不关心可见性判断Ajax插入的新节点visibility_of_element_located元素在DOM中且可见等待普通页面元素渲染完成element_to_be_clickable元素可见且可点击enabled点击按钮、链接之前必用presence_of_all_elements_located多个元素全部出现在DOM中等待列表数据加载完成text_to_be_present_in_element元素的文本包含指定内容等待接口返回后文本被填充text_to_be_present_in_element_value元素的value属性包含指定内容等待输入框值被程序回填staleness_of旧元素从DOM中脱离即元素失效等待页面刷新、路由切换frame_to_be_available_and_switch_to_itiframe可用并自动切入操作iframe内的元素alert_is_present弹窗出现等待Alert弹窗出现并处理element_to_be_selected下拉框/单选/复选处于选中状态等待选项被选中后继续断言用错条件的案例特别多比如presence_of_element_located和visibility_of_element_located经常被混淆。前者只要元素被createElement插入到文档就算满足哪怕它在屏幕外、display:none后者必须元素可见也就是拥有宽度高度并且没有隐藏属性。如果元素被前端框架渲染成隐藏状态你用了前者去等拿到的元素接下来做点击就会报错这点要格外注意。2.3 等待条件不满足时的异常处理逻辑显示等待最让人头疼的就是超时。一旦WebDriverWait.until在超时时间内没有等到条件满足会抛出TimeoutException。默认情况下这条异常信息只包含超时时长和元素定位表达式信息量有限。我习惯在每一个until调用里补上message参数把业务场景描述写进去。wait WebDriverWait(driver, 10) try: confirm_btn wait.until( EC.element_to_be_clickable((By.XPATH, //button[contains(text(), 确认付款)])), message点击确认付款按钮超时请确认收银台页面是否正常弹出 ) confirm_btn.click() except TimeoutException: # 补充截图、页面源码、当前URL等现场信息 driver.save_screenshot(timeout_pay.png) raise这样报错日志里一眼就能看到是哪一步、因为什么业务原因失败而不是笼统的Timed out after 10s。在持续集成里跑自动化时这种带上下文的报错信息能帮你省下大量排查时间。另外WebDriverWait还支持ignored_exceptions参数用于指定等待过程中要忽略的异常类型。比如有些页面元素在切换动画期间会短暂不可交互你可以把ElementNotInteractableException作为忽略项让轮询过程在遇到这类错误时不直接弹出而是继续等到条件满足或超时为止。wait WebDriverWait( driver, 15, ignored_exceptions(ElementNotInteractableException,) )2.4 超时时间与轮询频率怎么调才合理WebDriverWait(driver, timeout, poll_frequency0.5)这三个参数各有讲究。timeout是整个等待的最大时长我一般按接口耗时评估来定普通Ajax请求3到5秒加载报告这类重接口8到15秒。不要盲目设成30秒超时时间过长会让失败的用例拖很久才能报错一条还好几百条用例跑下来时间成本不可小觑。poll_frequency默认0.5秒大多数场景下不需要改动。如果页面本身渲染精细、动画较多可以调到0.2秒提升响应灵敏度如果页面元素状态切换很慢调到1秒反而能减少无谓的重复查找开销。还有一个经验不要在until里传一个会报NoSuchElementException的Lambda表达式因为WebDriverWait虽然会吞掉轮询过程中的某些异常但对自定义Lambda里直接崩出来的异常处理并不可靠。我见过不少新手这样写# 不推荐这种写法 wait.until(lambda d: d.find_element(By.ID, foo).is_displayed())如果find_element在元素缺失时抛异常Lambda的异常处理和轮询逻辑之间容易产生混乱。更好的做法是使用EC模块里的现成条件或者自定义一个不抛异常的判断方法下面一节会详细讲。3. 自定义等待条件与复杂场景实操3.1 自定义expected_condition的两种实现方式EC模块虽然覆盖了大部分场景但业务中总有奇怪又具体的要求等一个元素的class从loading变成loaded等一个数据表格的行数从0变成大于0等一个滑块验证码的拼图块进入可拖动状态。这时候就需要自己写等待条件。实现方式有两种。第一种是写一个普通函数接收driver参数返回True或Falsedef slider_ready(driver): try: slider driver.find_element(By.CLASS_NAME, slider-btn) return slider.is_displayed() and slider.is_enabled() except NoSuchElementException: return False wait.until(slider_ready)第二种是定义一个类实现__call__方法。这种方式的好处是可以在多个用例里复用还能在构造阶段接收参数class wait_for_element_attribute: def __init__(self, locator, attribute, expected_value): self.locator locator self.attribute attribute self.expected_value str(expected_value) def __call__(self, driver): try: element driver.find_element(*self.locator) return element.get_attribute(self.attribute) self.expected_value except NoSuchElementException: return False wait.until(wait_for_element_attribute((By.ID, submit-btn), aria-disabled, false))这个类体感上很像一个自定义EC条件写起来不复杂但非常实用。等属性变化、等样式状态切换都可以用这个套路搞定。3.2 处理动态渲染、Ajax和iframe场景动态渲染是显示等待的主战场。前端框架React、Vue等往往会异步从后端拉数据再渲染界面数据没回来之前页面上只有空壳。这时候我会用一个策略组合先用presence_of_element_located等根节点出现再用自定义条件等业务数据实际填充到界面上。比如订单列表页Ajax接口返回前页面显示加载中返回后渲染出表格行wait.until(EC.presence_of_element_located((By.CLASS_NAME, order-table))) orders wait.until( lambda d: d.find_elements(By.CSS_SELECTOR, .order-table tbody tr) ) if not orders: raise AssertionError(订单列表为空但接口未返回空数据)iframe是另一个高频坑。很多第三方组件支付、登录、报表都套在iframe里面。如果你没有切进去即使在iframe内层页面里等了半天find_element也照样找不到。正确做法用frame_to_be_available_and_switch_to_it一步到位iframe wait.until( EC.frame_to_be_available_and_switch_to_it((By.ID, pay-iframe)) ) inner_btn wait.until(EC.element_to_be_clickable((By.ID, confirm-pay)))这个等待条件会等待iframe出现在DOM中且加载到可交互状态然后自动把上下文切换进去省去了手动switch_to.frame的麻烦。操作完后记得switch_to.default_content()切回来否则后续操作全都会在错误的作用域里执行。3.3 文件上传、下载等特殊场景的等待策略Selenium操作文件上传通常是对input[typefile]元素直接send_keys本地路径这本身不复杂但要确保输入框是可交互状态再执行一般用presence_of_element_located就够了file_input wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, input[typefile]))) file_input.send_keys(/path/to/test_data.xlsx)真正费劲的是文件下载场景。浏览器点击下载按钮后会弹出下载任务但Selenium没法直接监听浏览器的下载完成事件。我自己常用的方案是轮询下载目录判断文件是否出现并且不再有.crdownload或.tmp后缀临时文件。把WebDriverWait当作一个通用轮询器来用也很顺import os download_dir /Users/me/Downloads def download_finished(driver): if not os.path.exists(download_dir): return False files os.listdir(download_dir) # Chrome下载中的文件会先出现 .crdownload 扩展名 if any(f.endswith(.crdownload) for f in files): return False return len(files) 0 wait.until(download_finished, message等待下载完成超时)用wait.until来轮询非浏览器状态有点曲线救国好处是复用同一套超时和异常处理逻辑。真正严格一点的做法是自己写一个time.monotonic()死循环判断文件是否达到预期大小且不再变化但如果只是常规测试判断.crdownload消失已经足够可靠。顺带提一句热词里有人问Selenium点击按钮就下载图片的代码怎么写本质思路就是先定位到下载按钮点击后等待文件出现在指定下载目录再对文件做断言。图片下载一般很快但网络慢时也会卡几秒不用等待策略就会导致脚本一执行完文件还没落地断言全挂。4. 显示等待的工程化落地与踩坑经验4.1 封装一个好用的等待工具类如果项目里到处是WebDriverWait裸调用超时时间东一个10秒西一个5秒一旦产品响应变慢你就要全局搜索替换。我建议在早期就把等待逻辑封装成工具类或基类方法。我这里提供一个参考实现from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: DEFAULT_TIMEOUT 10 def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, self.DEFAULT_TIMEOUT) def wait_element_visible(self, locator, timeoutNone): return self.wait.until( EC.visibility_of_element_located(locator), messagef等待元素可见超时: {locator} ) def wait_element_clickable(self, locator, timeoutNone): return self.wait.until( EC.element_to_be_clickable(locator), messagef等待元素可点击超时: {locator} ) def wait_element_text(self, locator, text, timeoutNone): return self.wait.until( EC.text_to_be_present_in_element(locator, text), messagef等待元素文本 {text} 超时: {locator} )这样业务页面继承BasePage之后所有等待都走统一方法超时时间也只在一处配置。需要针对某个接口调整超时时再单独传入timeout参数维护成本会低很多。4.2 与Page Object模式结合Page Object模式PO模式在UI自动化里几乎是标配了。它的核心思想是把页面元素定位和操作逻辑收敛到独立的页面类里测试用例只关心业务操作和断言。显示等待天然适合放到PO层定位元素时就用等待条件而不是到处把WebDriverWait塞进用例代码里。拿登录页来举例class LoginPage(BasePage): username_input (By.ID, username) password_input (By.ID, password) login_button (By.ID, login-btn) error_tip (By.CLASS_NAME, error-tip) def login(self, username, password): self.wait_element_visible(self.username_input).send_keys(username) self.wait_element_visible(self.password_input).send_keys(password) self.wait_element_clickable(self.login_button).click() def get_error_tip(self): return self.wait_element_visible(self.error_tip).text用例层写起来就非常干净def test_login_wrong_password(driver): login_page LoginPage(driver) login_page.login(tester, wrong_password) assert login_page.get_error_tip() 用户名或密码错误所有等待细节都被封装在页面对象内部了用例读起来像产品验收步骤而不是一堆find_element和wait。4.3 常见问题排查速查表做UI自动化久了遇到卡在等待上面的问题可以出一本《踩坑大全》。下面这个表是我备忘录里长期更新的内容列出来给大家参考。症状可能原因排查与解决方向元素明明在页面上等待却超时等错了条件比如该用element_to_be_clickable却用了presence_of_element_located核对元素状态改用visibility_of_element_located或element_to_be_clickable脚本跑得特别慢每一步都卡几秒隐式等待和显示等待同时存在轮询相互叠加全局移除implicitly_wait统一用显示等待偶发超时重跑就通过弱网或前端动画导致状态不稳定适当增加超时时间检查是否需要等待遮罩结束定位iframe内元素一直失败没有切换到iframe作用域用frame_to_be_available_and_switch_to_it等待并切换元素出现后又消失页面二次刷新或旧节点被替换用staleness_of等待旧元素失效再重新定位新元素点击报ElementClickInterceptedException元素可见但被遮罩或loading层遮挡先等遮罩消失再执行点击等待条件循环里一直抛异常自定义条件没有捕获NoSuchElementException在自定义条件里捕获异常并返回False文件断言时文件还没下载完点击下载后没有等文件落地轮询下载目录判断.crdownload临时文件消失这条表的核心启发在于等待超时不一定意味着元素没出现更多时候是你等待的条件和元素的真实状态不匹配。4.4 显示等待的性能影响与优化有人担心显示等待用多了会影响测试集执行效率这个担心方向是对的但要看你等的是条件还是时间。显示等待的特点是条件满足立即返回所以理想情况下它比固定sleep更快因为不会白白等待。但如果超时设置过大且页面真的有问题一个失败用例就会拖很久。对于大项目我建议做三件事一是清理所有隐式等待杜绝implicitly_wait和WebDriverWait并用的情况。根据我的实测两种方式混用时等待时长可能会变成隐式时长 显示等待内部轮询时长脚本整体耗时能被拉高20%到40%。二是超时时间差异化。页面首页、登录这种轻量场景用8秒以内报表、数据看板这种重接口场景用15到20秒文件下载单独设置30秒。全局一个超时值虽然省事但必然造成某些用例不必要地多等。三是优先等待业务目标状态而不是中间元素。比如你要等一个提交按钮从禁用变成可用就不要先去等loading图标消失、再去等按钮文本变化、再去判断可用性直接一次element_to_be_clickable就够了。等待链越短脚本越稳执行也越快。同样的道理能用until_not(EC.visibility_of_element_located(locator))等遮罩消失就不要在中间玩先sleep再找元素的杂技。做Selenium自动化到今天我对等待这件事最大的体会是它不只是API调用的问题更是测试设计思路的问题。一个稳定的自动化脚本靠的不是把超时时间调大、把等待次数堆多而是把页面到底什么时候才算真正就绪想清楚。显示等待恰恰提供了一种精确描述就绪的能力等可见就等可见等可点击就等可点击等旧元素失效就等旧元素失效。沿着这个思路去写脚本那些困扰大多数人的偶发超时和脚本脆断会少掉一大半。最后分享一个我长期坚持的小习惯每次排查等待问题我都会在定位到根因后把这次的教训补充进团队的等待条件速查表里而不是修完就算。积累一段时间后你会发现团队里的脚本稳定性指标不知不觉就上去了。
返回列表