Selenium自动化测试实战:从零构建百度搜索自动化脚本
1. 项目概述
如果你是一名测试工程师,或者是一名想提升工作效率的开发者,那么“自动化测试”这个词对你来说一定不陌生。但很多时候,我们听到的更多是概念和框架,真正动手时却不知从何开始,或者写出来的脚本脆弱不堪,一个页面元素的微小改动就能让它“罢工”。今天,我就以一个最经典、最接地气的场景——“自动化百度搜索”为例,带你从零开始,手把手拆解Web自动化测试的完整流程。这不仅仅是一个“Hello World”式的演示,我会把我在实际项目中踩过的坑、总结的经验,以及如何构建一个健壮、可维护的自动化脚本的思考,毫无保留地分享给你。无论你是想用自动化测试来保证产品质量,还是想用它来解放双手,做一些重复性的数据抓取或操作,这篇文章都能给你一套可以直接“抄作业”的实战方案。
2. 核心概念与工具选型
在动手写代码之前,我们必须先搞清楚两件事:第一,什么是Web自动化测试,我们到底要用它来做什么?第二,面对琳琅满目的工具,我们该如何选择?
2.1 Web自动化测试的本质与价值
Web自动化测试,简单说,就是用程序模拟人在浏览器里的操作,比如点击、输入、滚动、验证页面内容等。它的核心价值在于将重复、枯燥的回归测试工作交给机器,从而:
- 提升效率与覆盖率:一套脚本可以在几分钟内执行完人工需要数小时才能完成的测试用例,并且可以7x24小时不间断运行,极大地提高了测试覆盖率和执行频率。
- 保证一致性:人工测试难免会有疏忽和疲劳,而机器每次执行都完全一致,避免了人为误差。
- 支持持续集成:自动化测试脚本可以无缝集成到CI/CD流水线中,每次代码提交后自动运行,快速反馈质量问题,是实现DevOps和敏捷开发的关键一环。
对于我们今天的“百度搜索”实战,其价值可以具体化为:验证搜索功能是否正常、检查搜索结果的相关性、或者作为更复杂数据采集流程的第一步。理解了这个“为什么”,我们写代码时目标才会更明确。
2.2 主流工具对比与Selenium胜出理由
市面上Web自动化测试工具不少,比如Puppeteer、Cypress、Playwright,以及老牌的Selenium。为什么我选择Selenium作为入门和实战的首选工具?
- Selenium:它是一个开源项目,支持多种语言(Java, Python, C#, JavaScript等),几乎兼容所有现代浏览器。它的生态非常成熟,社区庞大,遇到问题基本都能找到解决方案。其核心原理是通过WebDriver协议与真实浏览器通信,因此测试环境最贴近真实用户。
- Puppeteer/Playwright:这两个是后起之秀,由浏览器厂商(Chrome/微软)直接支持,在速度和稳定性上有一定优势,特别是对现代Web应用(单页应用SPA)的支持更好。但它们更偏向于Node.js生态。
- Cypress:同样是一个优秀的现代测试框架,开箱即用,对前端开发者非常友好。但其运行机制与Selenium不同,不支持同时驱动多个浏览器标签页或跨域操作,有一定局限性。
选择Selenium的理由:
- 普适性最强:语言和浏览器支持最广,学习资料最多,是行业事实标准。掌握了Selenium,再学其他工具会非常容易。
- 最贴近真实用户:驱动真实浏览器,能测试到包括JavaScript渲染、CSS样式在内的完整用户体验,这是无头浏览器或一些模拟器无法完全替代的。
- 生态完善:除了核心的WebDriver,还有Selenium Grid用于分布式测试,IDE用于录制回放,以及众多成熟的框架(如TestNG, JUnit, Pytest)可以集成。
对于初学者和大多数企业级应用测试,从Selenium入手是稳妥且收益最大的选择。本次实战我们将使用Selenium with Python的组合,因为Python语法简洁,生态丰富,非常适合快速实现自动化脚本。
3. 环境搭建与核心配置详解
工欲善其事,必先利其器。环境搭建是第一步,也是新手最容易卡住的地方。我会详细说明每一步,并解释其作用。
3.1 Python与Selenium库安装
首先,确保你的系统安装了Python(建议3.7及以上版本)。然后通过pip安装Selenium库,这是最基础的一步。
pip install selenium这个命令会从Python官方的包索引下载并安装selenium包。安装成功后,你就能在Python代码中from selenium import webdriver了。
3.2 WebDriver:连接代码与浏览器的桥梁
这是最关键也是最容易出错的一步。Selenium本身不能直接控制浏览器,它需要通过一个名为“WebDriver”的中间件来发送指令。你需要为你要使用的浏览器下载对应的WebDriver。
- ChromeDriver(用于Chrome/Edge新版):访问 ChromeDriver官网 或国内的镜像站,下载与你的Chrome浏览器大版本号一致的驱动。比如你Chrome是115版本,就找115.x.x.x的ChromeDriver。
- GeckoDriver(用于Firefox):访问 GeckoDriver GitHub发布页 下载。
- Microsoft Edge Driver:如果使用Edge,需从 微软开发者网站 下载。
下载后,你需要告诉Selenium这个驱动放在哪里。有三种常见方式:
- 放入系统PATH:将下载的驱动文件(如
chromedriver.exe)放在系统环境变量PATH包含的目录下(如C:\Windows\或/usr/local/bin)。这是最推荐的方式,一劳永逸。 - 指定绝对路径:在代码中通过
webdriver.Chrome(executable_path=‘你的路径/chromedriver’)指定。但注意,最新版Selenium已弃用executable_path参数,推荐使用Service对象。 - 使用WebDriver Manager(强烈推荐):这是一个第三方库,可以自动帮你下载、匹配和管理WebDriver,省去手动下载和版本匹配的烦恼。安装:
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(service=service)
实操心得:在团队协作或CI/CD环境中,强烈推荐使用WebDriver Manager或将驱动放入PATH。这能避免因团队成员浏览器版本不一致导致的“在我机器上是好的”这类问题。手动指定路径的方式只适合临时调试。
3.3 编写你的第一个自动化脚本:打开百度
环境准备好后,我们来写一个最简单的脚本:打开百度首页。
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager import time # 1. 创建WebDriver服务(使用WebDriver Manager自动管理驱动) service = Service(ChromeDriverManager().install()) # 2. 实例化浏览器驱动对象,这里使用Chrome driver = webdriver.Chrome(service=service) # 3. 控制浏览器窗口(可选) driver.maximize_window() # 最大化窗口,确保元素可见 # 4. 打开目标网址 driver.get("https://www.baidu.com") # 5. 等待几秒,以便观察效果 time.sleep(3) # 6. 获取当前页面标题并打印 print("当前页面标题是:", driver.title) # 7. 关闭浏览器窗口 driver.quit()运行这段代码,你应该能看到一个Chrome浏览器自动打开,访问百度首页,然后在控制台打印出“当前页面标题是:百度一下,你就知道”,最后关闭。
代码解析:
driver对象是你控制浏览器的“遥控器”,所有后续操作都通过它进行。get(url)方法用于导航到指定URL。title属性用于获取当前页面的标题。quit()方法会关闭所有由该驱动打开的窗口并结束驱动进程。与之类似的close()方法只关闭当前标签页。务必在脚本最后调用quit()来清理资源,否则后台可能会残留浏览器进程。
4. 元素定位:自动化操作的基石
自动化测试的核心是“找到元素,然后操作它”。Selenium提供了多达8种元素定位方式,掌握它们是你从入门到精通的关键。
4.1 八大定位策略详解
假设我们要定位百度的搜索输入框(那个长长的文本框)。
- ID定位:最优先使用的方式,因为ID在HTML中通常是唯一的。
search_box = driver.find_element(By.ID, “kw”) # 百度输入框的id就是‘kw’ - Name定位:类似于ID,但Name可能不唯一。
search_box = driver.find_element(By.NAME, “wd”) # 百度输入框的name是‘wd’ - Class Name定位:通过CSS类名定位。注意,一个元素可能有多个类名,且类名常不唯一。
# 假设输入框有一个类名 ‘s_ipt’ search_box = driver.find_element(By.CLASS_NAME, “s_ipt”) - Tag Name定位:通过HTML标签名定位,如
<input>,<div>。通常用于查找一组同类元素。inputs = driver.find_elements(By.TAG_NAME, “input”) # 找到页面上所有input元素 - Link Text定位:专门用于定位超链接(
<a>标签),通过链接的完整文本。news_link = driver.find_element(By.LINK_TEXT, “新闻”) # 定位文本为“新闻”的链接 - Partial Link Text定位:通过链接的部分文本定位。
news_link = driver.find_element(By.PARTIAL_LINK_TEXT, “闻”) # 也能定位到“新闻”链接 - CSS Selector定位:功能强大且灵活,语法与前端CSS选择器一致。是除了ID之外最常用的定位方式。
# 通过id search_box = driver.find_element(By.CSS_SELECTOR, “#kw”) # 通过class search_box = driver.find_element(By.CSS_SELECTOR, “.s_ipt”) # 通过属性 search_box = driver.find_element(By.CSS_SELECTOR, “input[name=‘wd’]”) # 组合定位 search_box = driver.find_element(By.CSS_SELECTOR, “form#form > input#kw”) - XPath定位:最强大的定位方式,可以遍历XML/HTML文档的任何节点。当其他方式都失效时,XPath往往能救场。
# 绝对路径(脆弱,不推荐) search_box = driver.find_element(By.XPATH, “/html/body/div[1]/div[1]/div[5]/div[2]/div/form/span[1]/input”) # 相对路径+属性(推荐) search_box = driver.find_element(By.XPATH, “//input[@id=‘kw’]”) # 使用文本内容定位 news_link = driver.find_element(By.XPATH, “//a[text()=‘新闻’]”) # 包含某些属性或文本 search_box = driver.find_element(By.XPATH, “//input[contains(@class, ‘s_ipt’)]”)
注意事项:
find_element返回找到的第一个匹配元素,find_elements返回一个包含所有匹配元素的列表。定位不到元素时,find_element会抛出NoSuchElementException异常。
4.2 如何选择定位策略?一个优先级建议
在实际项目中,为了脚本的稳定性和可读性,我通常会遵循以下优先级:
- 首选ID:唯一且查找速度最快。
- 次选CSS Selector:灵活、速度快,且现代前端开发中CSS选择器很常见。
- 谨慎使用XPath:XPath功能强大,但性能相对较差,且容易因页面结构微小变动而失效(尤其是绝对路径)。仅在复杂层级或需要根据文本定位时使用。
- 避免使用不稳定的属性:如自动生成的类名(
class=‘js-xxxx-123’)、索引位置(如div[3]),这些在页面迭代时极易变化。 - 为关键元素添加测试专用属性:在开发阶段,可以和前端工程师约定,为重要的可操作元素添加
>from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager import time # 初始化驱动 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) driver.maximize_window() wait = WebDriverWait(driver, 10) # 创建显式等待对象,最多等10秒 try: # 1. 访问百度 driver.get("https://www.baidu.com") print("已访问百度首页") # 2. 定位搜索框并输入关键词 # 使用显式等待,确保元素加载完成再操作 search_input = wait.until( EC.presence_of_element_located((By.ID, "kw")) ) search_input.clear() # 清空输入框,避免残留内容 search_input.send_keys("自动化测试") # 输入文本 print("已输入搜索关键词:自动化测试") # 3. 定位搜索按钮并点击 # 同样使用显式等待 search_button = wait.until( EC.element_to_be_clickable((By.ID, "su")) ) search_button.click() print("已点击搜索按钮") # 4. 等待搜索结果页面加载完成 # 可以等待某个结果元素出现,作为页面加载完成的标志 wait.until( EC.presence_of_element_located((By.CSS_SELECTOR, “div.result”)) ) print(“搜索结果页面加载完成”) # 5. 获取搜索结果 # 百度搜索结果的标题通常包含在<h3>标签内,且class包含‘t’ result_titles = driver.find_elements(By.CSS_SELECTOR, “h3.t”) print(f“共找到 {len(result_titles)} 条搜索结果:”) for index, title_element in enumerate(result_titles, start=1): # 获取标题文本 title_text = title_element.text.strip() # 获取链接。注意:标题的父级<a>标签才是真正的链接 link_element = title_element.find_element(By.XPATH, “./parent::a”) link_url = link_element.get_attribute(“href”) print(f“{index}. 标题:{title_text}”) print(f“ 链接:{link_url}”) print(“-” * 50) # 6. 可选:翻页操作 # 定位“下一页”按钮并点击 # next_page_button = driver.find_element(By.LINK_TEXT, “下一页 >”) # next_page_button.click() # print(“已点击下一页”) # time.sleep(2) # 等待翻页加载 except Exception as e: print(f“执行过程中出现错误:{e}”) # 可以在这里截图,方便排查问题 driver.save_screenshot(“error_screenshot.png”) finally: # 确保无论是否出错,最后都关闭浏览器 time.sleep(3) # 最后等待3秒,方便观察结果 driver.quit() print(“浏览器已关闭”)5.2 核心强化:三种等待机制详解
Web自动化测试中,等待是保证脚本稳定性的灵魂。页面加载、元素出现、AJAX请求完成都需要时间。不当的等待会导致“元素未找到”的错误。Selenium主要有三种等待方式:
强制等待:
time.sleep(seconds)- 是什么:让脚本无条件暂停指定秒数。
- 缺点:时间固定,无论页面是否加载完成都要等,效率低下。网络或环境变化时,固定的时间可能不够或浪费。
- 使用场景:仅用于调试,或在极少数明确知道需要固定间隔的操作(如等待动画完成)时使用。生产脚本中应尽量避免。
隐式等待:
driver.implicitly_wait(seconds)- 是什么:为
driver对象设置一个全局的等待时间。在查找任何元素时,如果元素没有立即出现,WebDriver会轮询DOM(默认每0.5秒)直到找到该元素或超时。 - 优点:设置一次,对整个driver生命周期有效。
- 缺点:不够灵活,只能用于元素查找,无法处理其他条件(如元素可点击、元素包含特定文本)。并且,一旦设置,会影响所有的
find_element操作。 - 建议:可以设置一个较短的全局隐式等待(如5秒),作为基础保障,但不要依赖它处理复杂场景。
- 是什么:为
显式等待:
WebDriverWait(driver, timeout).until(condition)- 是什么:针对某个特定条件进行等待,条件满足则立即继续执行,超时则抛出异常。这是最推荐、最智能的等待方式。
- 核心:
expected_conditions模块提供了大量预定义条件,如:presence_of_element_located:元素出现在DOM中(不一定可见、可点击)。visibility_of_element_located:元素可见(宽高大于0)。element_to_be_clickable:元素可见且可点击。title_contains:页面标题包含特定文字。
- 优点:精准、高效、灵活。针对不同操作使用不同的等待条件,脚本健壮性大大提升。
- 示例:如上文代码中,我们在点击搜索按钮前,使用了
element_to_be_clickable条件,确保按钮确实可以点了才去点击。
最佳实践:显式等待为主,隐式等待为辅,强制等待慎用。通常我会在创建driver后设置一个较短的隐式等待(如5秒)作为兜底,然后在所有关键操作前使用显式等待。
5.3 高级操作:处理弹窗、滚动与执行JS
真实的网页往往更复杂。我们来看看如何处理一些常见场景。
处理JavaScript弹窗(Alert/Confirm/Prompt):
from selenium.webdriver.common.alert import Alert # 假设某个操作会触发一个确认框 driver.find_element(By.ID, “trigger-alert”).click() # 切换到弹窗 alert = Alert(driver) print(“弹窗文本:”, alert.text) # 接受(点击“确定”) alert.accept() # 或者取消(点击“取消”) # alert.dismiss() # 如果是Prompt弹窗(有输入框),还可以输入文本 # alert.send_keys(“输入的内容”) # alert.accept()滚动页面:有时需要操作的元素不在当前可视区域内,需要滚动。
from selenium.webdriver.common.action_chains import ActionChains # 方法1:使用JavaScript直接滚动到元素 element = driver.find_element(By.ID, “target-element”) driver.execute_script(“arguments[0].scrollIntoView(true);”, element) # true表示与窗口顶部对齐 time.sleep(1) # 等待滚动完成 # 方法2:使用JavaScript滚动到指定位置 driver.execute_script(“window.scrollTo(0, document.body.scrollHeight);”) # 滚动到底部 driver.execute_script(“window.scrollTo(0, 500);”) # 滚动到纵坐标500像素 # 方法3:使用ActionChains模拟鼠标滚轮(更贴近用户操作,但可能不兼容所有浏览器) actions = ActionChains(driver) actions.move_to_element(element).perform()执行任意JavaScript代码:
execute_script是Selenium的一把瑞士军刀,可以突破WebDriver API的限制。# 修改元素属性 driver.execute_script(“document.getElementById(‘kw’).value = ‘Selenium’;”) # 获取元素样式 bg_color = driver.execute_script(“return window.getComputedStyle(document.body).backgroundColor;”) # 处理复杂的鼠标事件或DOM操作 # ...6. 框架雏形:让脚本更健壮、可维护
一个直接写在
main函数里的脚本是脆弱的。随着测试用例增多,我们需要引入一些设计模式,让代码结构清晰、易于维护和扩展。6.1 使用Page Object Model设计模式
POM是目前最主流的自动化测试设计模式。其核心思想是将页面抽象成类,将页面元素抽象成类的属性,将页面操作抽象成类的方法。这样,测试脚本(业务逻辑)就和页面细节(定位器)分离开了。
以百度首页和搜索结果页为例:
pages/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_element(self, by, locator): """查找单个元素,加入显式等待""" return self.wait.until(EC.presence_of_element_located((by, locator))) def find_elements(self, by, locator): """查找多个元素""" self.wait.until(EC.presence_of_element_located((by, locator))) return self.driver.find_elements(by, locator) def click(self, by, locator): """点击元素,确保可点击""" element = self.wait.until(EC.element_to_be_clickable((by, locator))) element.click()pages/baidu_home_page.py(百度首页)from selenium.webdriver.common.by import By from pages.base_page import BasePage class BaiduHomePage(BasePage): # 页面元素定位器 SEARCH_INPUT = (By.ID, “kw”) SEARCH_BUTTON = (By.ID, “su”) def __init__(self, driver): super().__init__(driver) self.driver.get(“https://www.baidu.com”) def search_for(self, keyword): """在搜索框输入关键词并点击搜索""" self.find_element(*self.SEARCH_INPUT).clear() self.find_element(*self.SEARCH_INPUT).send_keys(keyword) self.click(*self.SEARCH_BUTTON) # 返回搜索结果页对象,实现页面跳转的链式调用 return BaiduSearchResultPage(self.driver)pages/baidu_search_result_page.py(百度搜索结果页)from selenium.webdriver.common.by import By from pages.base_page import BasePage class BaiduSearchResultPage(BasePage): # 搜索结果标题元素 RESULT_TITLES = (By.CSS_SELECTOR, “h3.t”) def get_all_result_titles(self): """获取所有搜索结果的标题文本""" title_elements = self.find_elements(*self.RESULT_TITLES) return [title.text for title in title_elements] def get_all_result_links(self): """获取所有搜索结果的链接""" title_elements = self.find_elements(*self.RESULT_TITLES) links = [] for title in title_elements: link = title.find_element(By.XPATH, “./parent::a”).get_attribute(“href”) links.append(link) return linkstests/test_baidu_search.py(测试脚本)import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from pages.baidu_home_page import BaiduHomePage class TestBaiduSearch: def setup_method(self): """每个测试方法开始前执行""" service = Service(ChromeDriverManager().install()) self.driver = webdriver.Chrome(service=service) self.driver.maximize_window() def teardown_method(self): """每个测试方法结束后执行""" self.driver.quit() def test_search_automation(self): """测试百度搜索‘自动化测试’""" home_page = BaiduHomePage(self.driver) # 链式调用:从首页搜索,直接进入结果页 result_page = home_page.search_for(“自动化测试”) titles = result_page.get_all_result_titles() links = result_page.get_all_result_links() assert len(titles) > 0, “未找到任何搜索结果” print(f“搜索到 {len(titles)} 条结果。”) # 可以添加更具体的断言,比如检查标题中是否包含关键词 assert any(“自动化” in title for title in titles), “搜索结果中未包含预期关键词” if __name__ == “__main__”: # 简单运行 test = TestBaiduSearch() test.setup_method() test.test_search_automation() test.teardown_method()POM的优势:
- 高可维护性:页面元素定位器集中管理,前端页面改版时,只需修改对应的Page Class,测试脚本几乎不用动。
- 高可读性:测试脚本读起来就像自然语言
home_page.search_for(“xxx”),业务逻辑清晰。 - 低冗余:公共操作(如等待、查找)封装在基类中,避免代码重复。
6.2 数据驱动测试
将测试数据(如搜索关键词、断言期望值)从脚本中分离出来,存放在外部文件(如JSON, CSV, Excel)或代码中,使一套脚本可以执行多组数据测试。
简单示例(使用Python列表):
import pytest # 测试数据 search_data = [ (“自动化测试”, True), # 关键词, 期望找到结果 (“Selenium”, True), (“一个不存在的稀奇古怪关键词xyz”, False), # 期望没有结果 (“”, False), # 空搜索 ] @pytest.mark.parametrize(“keyword, expected_to_have_results”, search_data) def test_search_with_different_keywords(keyword, expected_to_have_results): # ... 初始化driver和page ... home_page = BaiduHomePage(driver) result_page = home_page.search_for(keyword) titles = result_page.get_all_result_titles() if expected_to_have_results: assert len(titles) > 0, f“搜索‘{keyword}’时期望有结果,但实际没有。” # 可以进一步断言标题相关性 else: # 对于无结果或空搜索,百度可能展示提示信息或无结果页,这里简化处理 # 实际应根据页面具体行为断言 print(f“关键词 ‘{keyword}’ 的搜索结果数量为:{len(titles)}”)7. 常见问题排查与实战技巧
即使按照最佳实践编写脚本,在实际运行中还是会遇到各种问题。这里分享一些高频问题的排查思路和技巧。
7.1 元素定位失败:NoSuchElementException
这是最常见的问题。排查步骤:
- 检查定位器:首先确认你的定位器(XPath/CSS)是否正确。在浏览器开发者工具的Console中,可以用
$$(“你的CSS”)或$x(“你的XPath”)快速测试是否能找到元素。 - 检查等待:元素还没加载出来你就去找它了。增加显式等待,并选择合适的等待条件(如
visibility_of_element_located)。 - 检查iframe/Shadow DOM:如果元素在
<iframe>或Shadow DOM内部,你需要先切换到对应的上下文。- 切换iframe:
iframe = driver.find_element(By.TAG_NAME, “iframe”) driver.switch_to.frame(iframe) # 在iframe内操作元素 # ... driver.switch_to.default_content() # 操作完切回主文档 - 处理Shadow DOM:需要使用JavaScript来穿透。
shadow_host = driver.find_element(By.CSS_SELECTOR, “#shadow-host”) shadow_root = driver.execute_script(“return arguments[0].shadowRoot”, shadow_host) inner_element = shadow_root.find_element(By.CSS_SELECTOR, “.inner-element”)
- 切换iframe:
- 检查页面是否发生跳转或刷新:页面跳转后,之前的元素引用会失效。需要在操作前重新定位。
- 检查浏览器窗口大小:某些响应式页面,元素在小窗口下可能被隐藏或布局改变。尝试
driver.maximize_window()。
7.2 脚本运行不稳定:有时成功有时失败
这通常是竞态条件导致的,即脚本执行速度与页面响应速度不匹配。
- 根治方法:用显式等待替代所有固定的
sleep。确保每个依赖页面状态的操作之前,都等待其前置条件满足。 - 重试机制:对于某些特别不稳定的操作(如网络请求),可以引入简单的重试逻辑。
from selenium.common.exceptions import StaleElementReferenceException import time def click_with_retry(element_locator, retries=3): for attempt in range(retries): try: element = wait.until(EC.element_to_be_clickable(element_locator)) element.click() return True except StaleElementReferenceException: # 元素引用失效,可能是DOM更新了,等待一下再试 if attempt < retries - 1: time.sleep(0.5) else: raise
7.3 浏览器行为与预期不符
- 弹窗拦截:有些操作(如下载、新窗口打开)可能会被浏览器拦截。需要提前更改浏览器选项。
from selenium.webdriver.chrome.options import Options chrome_options = Options() prefs = { “download.default_directory”: “/path/to/download”, # 设置下载路径 “download.prompt_for_download”: False, # 禁止下载提示 “profile.default_content_setting_values.automatic_downloads”: 1 # 允许多文件下载 } chrome_options.add_experimental_option(“prefs”, prefs) driver = webdriver.Chrome(options=chrome_options) - 证书错误/不安全提示:访问HTTPS站点遇到证书问题时。
chrome_options = Options() chrome_options.add_argument(‘--ignore-certificate-errors’) chrome_options.add_argument(‘--allow-insecure-localhost’) # 允许本地不安全连接 - 浏览器通知:禁用通知。
chrome_options.add_experimental_option(“prefs”, { “profile.default_content_setting_values.notifications”: 2 # 2代表阻止 })
7.4 提升脚本执行速度与资源管理
- 使用无头模式:不需要看到浏览器UI时,使用无头模式可以大幅节省资源,特别适合CI/CD环境。
chrome_options = Options() chrome_options.add_argument(“--headless”) # 启用无头模式 chrome_options.add_argument(“--disable-gpu”) # Windows系统可能需要 chrome_options.add_argument(“--no-sandbox”) # Linux系统有时需要 chrome_options.add_argument(“--disable-dev-shm-usage”) # 解决共享内存问题 - 及时清理:确保在
finally块或测试框架的teardown方法中调用driver.quit(),防止残留进程。 - 复用浏览器会话:对于需要登录的复杂测试,可以考虑复用用户数据目录,避免每次重新登录。
chrome_options.add_argument(f”user-data-dir={os.path.expanduser(‘~’)}/chrome_test_profile”)
8. 从脚本到测试:集成与报告
单个脚本跑通只是开始,我们需要将其纳入到完整的测试流程中,并生成清晰的测试报告。
8.1 使用Pytest测试框架
Pytest比Python自带的unittest更简洁强大。安装:
pip install pytest。- 组织测试用例:按照POM模式,将测试脚本放在
tests目录下,文件名以test_开头,函数名也以test_开头。 - 使用Fixture管理驱动生命周期:Pytest的
fixture可以优雅地实现setup和teardown。# conftest.py (放在项目根目录或tests目录下) import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager @pytest.fixture(scope=“function”) # 每个测试函数执行一次 def driver(): service = Service(ChromeDriverManager().install()) _driver = webdriver.Chrome(service=service) _driver.maximize_window() yield _driver # 测试函数执行时,传入_driver _driver.quit() # 测试函数执行完后,执行quit # test_baidu.py def test_search(driver): # driver fixture会自动注入 home_page = BaiduHomePage(driver) result_page = home_page.search_for(“pytest”) assert len(result_page.get_all_result_titles()) > 0 - 运行测试:在项目根目录下执行
pytest命令即可自动发现并运行所有测试。可以加参数,如pytest -v(详细输出)、pytest --html=report.html(生成HTML报告,需安装pytest-html)。
8.2 生成美观的测试报告
清晰的报告能直观反映测试结果。除了
pytest-html,Allure是更专业的选择。- 安装Allure:需要先安装Java,然后从 Allure官网 下载命令行工具并配置PATH。
- 安装Pytest的Allure适配器:
pip install allure-pytest。 - 运行测试并生成报告:
Allure报告会展示测试用例的通过率、失败原因、执行步骤、甚至截图,非常利于问题定位。pytest --alluredir=./allure-results # 运行测试,生成原始数据 allure serve ./allure-results # 在本地启动服务查看报告 # 或生成静态HTML报告 allure generate ./allure-results -o ./allure-report --clean
8.3 集成到CI/CD流水线
自动化测试的价值在CI/CD中才能最大化体现。以GitHub Actions为例,可以创建一个工作流文件
.github/workflows/test.yml:name: Web Automation Tests on: [push, pull_request] # 在代码推送或PR时触发 jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ‘3.9’ - name: Install dependencies run: | pip install -r requirements.txt pip install pytest allure-pytest - name: Install Chrome and ChromeDriver run: | sudo apt-get update sudo apt-get install -y wget wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - echo “deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main” | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y google-chrome-stable # 使用webdriver-manager,无需单独安装ChromeDriver - name: Run tests with Allure run: | pytest --alluredir=allure-results - name: Upload Allure report uses: actions/upload-artifact@v2 if: always() # 即使测试失败也上传报告 with: name: allure-report path: allure-results/这样,每次代码提交都会自动运行你的Web自动化测试,并将结果报告保存下来,团队任何成员都可以查看,实现了质量的持续反馈。
从打开一个浏览器,到定位一个元素,再到构建一个健壮的POM框架,最后集成到CI/CD流水线中生成专业报告,这就是一个完整的Web自动化测试从入门到实战的路径。这条路没有捷径,最大的技巧就是“动手写,动手调,多思考”。当你成功地将第一个自动化脚本融入团队的工作流,并看着它每天自动守护着产品的质量时,你会觉得这一切的投入都是值得的。自动化测试不是银弹,它无法替代探索性测试和人的智慧,但它是最可靠的守夜人,能把你从重复的劳动中解放出来,去解决更复杂、更有趣的问题。
