Selenium加Python这套组合,在自动化测试圈子里早就不是什么新鲜话题了,但直到今天,它依然是入门首选、项目落地最稳的方案之一。我见过太多人一开始就去追各种花哨的测试平台、录制回放工具,最后绕了一圈回来,发现还是老老实实写Selenium脚本最靠谱。这篇文章我就从实际干活的角度,把Selenium加Python实现基本自动化测试这件事彻底讲透,包括环境怎么搭、脚本怎么写、定位怎么选、坑怎么踩,完整走一遍,保证你看完能直接上手自己写。
1. 为什么是Selenium加Python:这套组合解决什么问题
1.1 自动化测试到底在解决什么痛点
在聊技术选型之前,得先搞清楚我们做自动化测试的初衷是什么。手工测试的痛点相信大家都深有体会:一个产品迭代到了中后期,每次发版前都要把核心流程从头到尾点一遍,登录、注册、下单、支付、退款,光这些主流程就要耗费大半天。更折磨人的是,越是临近上线,越要频繁回归测试,点得人想吐,而且人总会疲劳,点漏了、看走眼了,bug就溜到线上去了。
自动化测试的核心价值,就是把这部分重复性极高、人工执行容易出错的工作,交给脚本来完成。脚本的优势在于:执行速度快,一台机器可以同时跑多份脚本;二十四小时不休息,晚上提交代码,早上起来看测试报告;每次执行的结果都是稳定的,不会因为人的状态波动而出现漏测。所以,那些需要反复回归验证的功能、跨版本稳定性要求高的模块,都是自动化测试最适合发挥的场景。
1.2 技术选型的逻辑:为什么Selenium能成为事实标准
市面上做UI自动化的工具和框架不算少,QTP、RFT、Cypress、Playwright,各有各的粉丝。但Selenium能一直占据主流位置,核心原因有三个。
第一,它支持的语言足够丰富。Java、Python、C#、JavaScript、Ruby、Kotlin,基本主流开发语言都有对应的Selenium库。这意味着团队用什么语言写业务代码,就能用什么语言写自动化脚本,不需要为了测试专门去学一门陌生的技术栈。第二,它兼容所有主流浏览器。Chrome、Firefox、Edge、Safari,甚至老旧的IE,都有对应的driver驱动。对于需要兼容多种浏览器环境的企业应用来说,这一点非常关键。第三,它的社区生态太庞大了。你在使用过程中遇到任何报错、任何定位难题,基本都能在搜索引擎里找到现成的答案。
1.3 Python作为搭档的化学反应
语言绑定丰富是Selenium的优势,但为什么偏偏Python和它搭配起来最顺?因为Python写自动化脚本实在太舒服了。Selenium本身封装了大量的WebDriver API,Python的语法又简洁直白,两三百行的Java代码用Python写可能只要一百来行,开发和维护成本大幅降低。而且Python的第三方库生态给自动化测试提供了强大的外围支撑:pytest做用例管理和断言,requests做接口请求,Pandas处理测试数据,Allure生成精美的测试报告,这些工具配合起来,能搭建出一套相当完整的自动化测试体系。
所以,Selenium加Python的组合,最适合三类人:刚接触自动化测试的初学者,想快速把回归测试跑起来的测试工程师,以及需要自己动手写测试工具的开发人员。即使是没有任何编程基础的人,按照下面的步骤一步一步来,也能在短时间内跑通属于自己的自动化脚本。
2. 环境搭建:Windows和macOS下从零开始准备
2.1 Python安装的详细步骤与版本选择
这一步很多人觉得简单,但恰恰是新手最容易出问题的环节。去Python官网下载安装包时,一定要注意勾选安装界面底部的“Add Python to PATH”选项,这个勾选直接决定了你在命令行里能不能直接使用python命令。如果忘记勾选,后面每执行一次python命令都要写全路径,非常痛苦。
版本选择上,我的建议是直接上Python 3.10以上的稳定版本。目前Selenium 4.x对Python 3.7到3.12都支持得不错,但3.8以下的版本已经停止维护了。之前我遇到过同事因为装了Python 3.6,导致selenium库的某些新特性无法使用的情况,排查了半天才发现是版本太老。
安装完成后,打开命令行工具,Windows用Win+R输入cmd,macOS用Terminal,输入python --version,如果正确显示版本号,说明安装成功。这时候顺手把pip也检查一下,输入pip --version,确认pip可用。pip是Python的包管理工具,后面安装Selenium就靠它。
2.2 Selenium库与浏览器驱动的配对安装
环境准备好之后,安装Selenium库只需要一条命令:
pip install selenium如果你下载速度很慢,可以换成国内镜像源,比如清华源的地址:
pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple这里要特别说一下Selenium 4和3之间的一个重大差异。Selenium 4开始,官方把浏览器驱动管理功能内置了,引入了Selenium Manager机制。这意味着Chrome浏览器如果保持自动更新,Selenium会自动下载匹配的driver驱动,你不再需要手动去下载chromedriver并配置路径了。这绝对是个福音,早期用Selenium 3的时候,每次浏览器更新导致driver不匹配的报错,真的是让人崩溃。
当然,如果你用的浏览器比较小众,或者公司网络环境受限导致Selenium Manager下载不了驱动,那还是需要手动去对应的driver下载地址,把驱动文件放到项目目录下,或者放到Python的安装目录里。Chrome的driver对应关系,主要看浏览器主版本号,下载时必须精确匹配。
2.3 一个简单的验证脚本,跑通第一行自动化代码
环境配置完后,先别急着去搞复杂的东西,写一段最简脚本验证一下整个链路是否畅通。用记事本或者任何文本编辑器,创建一个demo.py文件,输入以下内容:
from selenium import webdriver driver = webdriver.Chrome() driver.get("https://www.baidu.com") print(driver.title) driver.quit()这是一段最基础的自动化脚本,逻辑只有四步:创建一个Chrome浏览器的驱动实例,打开百度首页,打印页面标题,关闭浏览器。执行方式很简单,在命令行里切到demo.py所在目录,输入python demo.py。执行后,如果你的电脑屏幕上真的弹出了一个Chrome窗口,并且终端里打印出了“百度一下,你就知道”,那恭喜你,环境搭建成功了,你已经完成了人生中第一个自动化测试脚本。
需要注意的是,第一次执行这段代码的时候,Selenium Manager会自动下载ChromeDriver,可能需要等十几秒到几分钟,取决于网络状况。如果卡住或者报错,八成是网络问题,可以手动设置环境变量或者下载驱动后放到指定位置。
3. 元素定位:自动化测试的核心基本功
3.1 八大定位方式与使用场景
脚本能打开浏览器只是第一步,自动化测试真正的工作,是让浏览器代替人手去操作页面上的各种元素,比如输入用户名、点击按钮、选择下拉项。而这一切的前提,是代码能准确地找到这些元素。
Selenium提供了八种元素定位方式:id、name、class_name、tag_name、link_text、partial_link_text、xpath、css_selector。实际操作中,使用频率最高的集中在三种:id、xpath、css_selector。id是元素在HTML中的唯一标识,如果前端同学规范地写了id属性,那直接用id定位是最简单也最稳定的方式。但现实情况是,很多前端代码并不规范,尤其是动态渲染的页面,id往往是随机生成的,这时候就要靠xpath和css_selector来救命了。
from selenium.webdriver.common.by import By # 用id定位 element = driver.find_element(By.ID, "username") # 用name定位 element = driver.find_element(By.NAME, "password") # 用class_name定位 element = driver.find_element(By.CLASS_NAME, "btn-login") # 用xpath绝对路径定位(不推荐,但有时候只能这样) element = driver.find_element(By.XPATH, "/html/body/div[1]/form/input[1]") # 用xpath相对路径定位(推荐) element = driver.find_element(By.XPATH, "//input[@placeholder='请输入用户名']") # 用css_selector定位 element = driver.find_element(By.CSS_SELECTOR, "#username") element = driver.find_element(By.CSS_SELECTOR, "input[name='username']")3.2 XPath定位的策略选择:杜绝脆弱的绝对路径
用xpath定位元素,最大的区别在于用“绝对路径”还是“相对路径”。绝对路径是从html根节点开始一直索引到目标元素,写法是/html/body/div/...,只要页面层级稍微调整一下,这个定位就失效了,脚本的稳定性极差,这活儿我早就不这么干了。相对路径是从任意一个稳定的父级节点开始,通过元素的属性、文本内容或者层级关系,用双斜杠//开头来找目标元素,这样即使页面其他地方发生了变动,只要目标元素的特征没变,脚本还能正常工作。
常见的相对路径写法,我总结了一些在项目里反复用的:
# 通过元素属性定位,最常用 //input[@id='username'] # 通过元素文本内容定位,适合定位按钮、链接 //button[text()='登录'] # 通过部分属性值匹配定位,适合属性值动态变化的元素 //div[contains(@class, 'modal-content')] # 通过层级关系定位,适合没有独立属性的元素 //div[@class='form-item']/input3.3 CSS Selector定位:性能更优的选择
相比xpath,css_selector语法更简洁,执行效率也更高,因为浏览器对CSS选择器的原生支持更好。它的核心语法跟CSS样式选择器一致:id用#前缀,class用.前缀,标签直接写标签名。实际工作中,我的习惯是能简洁用css,遇到复杂的结构化定位或者要靠文本内容定位时,再切换成xpath。
# id定位 By.CSS_SELECTOR, "#username" # class定位 By.CSS_SELECTOR, ".btn-login" # 属性定位 By.CSS_SELECTOR, "input[name='username']" # 子元素定位 By.CSS_SELECTOR, "div.form-item > input" # 多个class同时匹配 By.CSS_SELECTOR, "input.form-control.input-lg"3.4 特殊场景处理:非原生下拉框与文件上传
处理普通的select下拉框,Selenium有专门的Select类支撑,代码非常简洁:
from selenium.webdriver.support.ui import Select select_element = Select(driver.find_element(By.ID, "city")) select_element.select_by_index(1) select_element.select_by_value("beijing") select_element.select_by_visible_text("北京")但实际开发中,很多前端框架为了实现自定义样式,会把原生select藏起来,用div加ul加li的组合来模拟下拉框。这种场景用Select类就不管用了,只能走模拟用户操作的路线:先点击div触发下拉列表展示,再定位ul里的li项点击。下面这段代码就是处理这种情况的典型写法:
# 点击下拉框,展开选项列表 driver.find_element(By.CSS_SELECTOR, "div.select-box").click() time.sleep(0.5) # 定位并点击目标选项 driver.find_element(By.XPATH, "//ul[@class='select-options']//li[text()='北京']").click()文件上传也是一个高频场景,处理起来有一个很重要的技巧。对于input标签的文件上传按钮,直接用send_keys方法把本地文件路径传进去,根本不需要真的去点击文件选择弹窗:
# 本地文件上传:直接传入文件绝对路径 driver.find_element(By.ID, "uploadFile").send_keys("C:\\Users\\test\\Desktop\\testfile.pdf")这个技巧能解决90%的文件上传场景,因为大多数上传按钮底层都是input标签。当然,如果是那种点击后弹出拖拽区域、需要拖拽才能上传的,那就需要借助其他工具或者模拟键盘操作了,不过那种属于少数派,日常工作中遇到再单独处理就行。
3.5 定位不到元素的排查思路
定位不到元素是自动化测试最常遇到的报错,核心报错信息就是NoSuchElementException。每次遇到这个报错,我的排查顺序固定是三步。第一步,打开浏览器开发者工具,按F12或者右键选择“检查”,用Elements面板里的选择器工具定位目标元素,确认它的属性值和我的定位表达式完全一致。第二步,检查目标元素是不是在iframe里面,如果在iframe里,必须先切换进去,否则永远定位不到:
# 切换iframe driver.switch_to.frame("iframe_id_or_name") # 操作完切回来 driver.switch_to.default_content()第三步,考虑元素是不是动态渲染的,代码执行速度比页面加载速度快,元素还没渲染出来就去定位了,这时候就需要等待机制登场了。这是下一节的重点内容。
4. 等待机制:让脚本从“时好时坏”变成“稳定可靠”
4.1 三种等待方式的对比与选择
很多新手写的脚本,跑一次成功一次失败,而且失败的点还经常不一样,大部分原因都是没处理好等待机制。页面加载和元素渲染都是需要时间的,尤其是现代前端应用,大量使用Ajax异步请求,元素出现的时间点很难预测。Selenium提供了三种等待方式。
强制等待是最简单粗暴的方式,就是让程序强制休眠指定的秒数:
import time time.sleep(3)这种方式虽然简单,但是缺点很明显:一是时间不好预估,给短了页面还没加载完,给长了白白浪费时间;二是脚本执行效率大幅下降,测试一百个用例,每个等三秒,光等待就浪费了五分钟。我把强制等待称为“新手病”,不是说完全不能用,只是它不适合作为主要方案。
隐式等待在设置之后的整个driver生命周期内都会生效,每次定位元素时,如果元素没立即出现,会在设定的时间内持续尝试查找:
driver.implicitly_wait(10)这样设置以后,driver在10秒内如果定位到元素就继续执行,如果10秒后仍然找不到,就抛出异常。这种方式的优点是设置一次全局生效,省心。缺点是它只对元素出现生效,对元素的其他状态,比如可点击、可见等,是无能为力的。
显式等待是目前最推荐的等待方式,它针对某一个特定元素,设置一个等待条件和最大等待时间:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待元素可见,最多等10秒,每0.5秒检查一次 element = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, "loginButton")) )4.2 显式等待的正确打开方式
显式等待还有一点好处,是它可以跟多个不同的条件配合使用。除了元素可见visibility_of_element_located,常见的还有元素存在于DOM树中presence_of_element_located、元素可点击element_to_be_clickable、元素包含特定文本text_to_be_present_in_element等。这些都是平时用的非常高频的条件。
常见的业务场景是:点击一个按钮之后,页面上会弹出一个操作成功的提示,然后跳转到下一个页面。这种场景就可以这样处理:
# 先用显式等待确保按钮可点击再点击,避免按钮还在禁用状态 login_button = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "loginBtn")) ) login_button.click() # 等待跳转后的页面元素出现 dashboard = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.XPATH, "//h1[text()='控制台']")) )4.3 为什么显式等待能显著提高脚本稳定性
显式等待的优势还在于它的精准性。它等待的不是一个固定时间,而是一个条件满足。页面加载快时,它毫不拖延地继续执行;页面加载慢时,它就耐心等到超时为止。这种“按需等待”的方式,既保证了稳定性,又不会浪费多余的时间。同时,显式等待在找不到元素时,报错信息也更直观,它会明确指出等待了10秒后元素仍不满足预期条件,方便我们定位问题。
在实际项目里,我的习惯是建立一个统一的工具类,把常见的等待封装成方法,避免在每个测试用例里重复写WebDriverWait那一大串代码。比如这样:
import time from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def wait_clickable(driver, timeout, locator): """等待元素可点击并返回元素""" return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) # 使用示例 from selenium.webdriver.common.by import By login_btn = wait_clickable(driver, 10, (By.ID, "loginBtn")) login_btn.click()这样封装之后,不仅代码更简洁,而且一旦某个元素超时了,报错信息读起来也直观得多。等以后用例多了,维护成本也会明显低很多。
5. 完整实操:用登录场景跑通一套自动化测试流程
5.1 设计一个可复用的登录测试用例
前面讲了这么多基础,现在来一个完整的实操。这一节我以最常见的登录功能为例,从用例设计到脚本编写,完整地走一遍。登录功能是几乎所有项目都具备的基础功能,测试点无非这么几类:输入正确的用户名和密码能登录成功;输入错误的用户名或密码会提示错误信息;用户名为空、密码为空、用户名密码都为空时,分别有对应的提示;密码输入框是否支持复制粘贴、是否有掩码保护;记住密码、忘记密码功能是否正常。
把这些测试点整理成测试用例,再结合Selenium的运行逻辑,我们可以设计一个基本的用例清单:
| 用例编号 | 测试场景 | 操作步骤 | 预期结果 |
|---|---|---|---|
| TC_001 | 正确登录 | 输入正确用户名密码,点击登录 | 跳转到首页,显示用户昵称 |
| TC_002 | 错误密码 | 输入正确用户名,错误密码 | 页面提示“用户名或密码错误” |
| TC_003 | 空用户名 | 不填用户名,直接点击登录 | 页面提示“请输入用户名” |
| TC_004 | 空密码 | 填用户名,不填密码 | 页面提示“请输入密码” |
| TC_005 | 大小写敏感 | 输入正确用户名,改变密码大小写 | 登录失败,提示错误信息 |
5.2 登录脚本的完整实现与逐行说明
基于上面的用例,我们来写第一个相对完整的自动化测试脚本。这个脚本会覆盖登录成功和登录失败两条主路径:
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.implicitly_wait(5) try: # 打开目标测试页面 driver.get("https://example.com/login") # 定位用户名输入框,输入用户名 username_input = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, "username")) ) username_input.clear() username_input.send_keys("test_user") # 定位密码输入框,输入密码 password_input = driver.find_element(By.ID, "password") password_input.clear() password_input.send_keys("Test123456") # 点击登录按钮 login_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "loginBtn")) ) login_btn.click() # 等待登录成功后的用户昵称元素出现 try: nickname = WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.XPATH, "//div[@class='user-info']//span[@class='nickname']")) ) print("登录成功,当前用户是:", nickname.text) except: # 如果昵称元素没有出现,检查是否出现错误提示 error_msg = WebDriverWait(driver, 5).until( EC.visibility_of_element_located((By.CSS_SELECTOR, ".error-message")) ) print("登录失败,错误提示:", error_msg.text) finally: # 无论执行结果如何,最终都关闭浏览器 driver.quit()这段脚本其实蕴含了多个设计要点。第一,用了显式等待去等待关键元素,而不是一上来就sleep固定时间,这样既快又稳。第二,登录成功和登录失败的断言都做了,如果登录成功就去验证用户昵称,如果昵称没出现再去查找错误提示,这样无论页面走向哪条分支,脚本都能给出有意义的输出。第三,用了try-finally结构,即使脚本中间抛出了异常,浏览器也能被正常关闭,不会留下僵尸进程。
5.3 引入断言:让脚本真正成为测试
刚才这个脚本还稍微粗了一点,因为我们只把结果打印出来了,还没有严格判断测试用例是通过还是失败。要让脚本真正具备测试能力,必须加入断言机制。Python内置的assert关键字就能做最基础的断言:
# 登录成功断言:用户昵称必须包含预期文本 assert "test_user" in nickname.text, f"登录成功但用户昵称不符合预期,实际昵称为:{nickname.text}" # 登录失败断言:错误提示必须包含预期文本 assert "用户名或密码错误" in error_msg.text, f"错误提示不符合预期,实际提示为:{error_msg.text}"代码执行到断言这一行时,如果断言条件为False,程序会抛出AssertionError,测试用例就会被标记为失败。这就是自动化测试的基石逻辑:脚本不再仅仅是帮我们“操作页面”,而是能替我们“判断结果是否正确”。
更规范的做法是用pytest等测试框架来管理用例,把每个测试场景写成独立的函数,用pytest的断言语法no_message来标记用例状态。这里就不过度展开了,但思路要清楚:Selenium负责执行页面操作,测试框架负责组织和断言,两者配合,才能形成一个完整的自动化测试工程。
5.4 测试报告的生成与结果分析
脚本跑完以后,怎么把结果直观地呈现给团队,也是一门学问。最简单的做法是浏览器控制台输出加文件日志,进阶做法是用pytest配合Allure生成图文并茂的HTML测试报告。Allure报告支持展示每个用例的执行状态、耗时、失败原因、失败截图,还能按模块、按优先级对用例进行聚合统计。
拿到测试报告并不可怕,可怕的是有失败用例不知道从哪看起。我的习惯是:首先看失败原因,如果报错是NoSuchElementException,优先怀疑定位问题;如果报错是AssertionError,优先怀疑业务逻辑或者数据问题,打开失败用例的截图,对比实际页面和预期描述,就能快速定位问题。
6. 实战避坑:那些文档里不会写的Selenium经验
6.1 常见问题与排查速查表
排查问题这件事,经验越丰富就越能快速定位。这里我做了一个基于实际项目经验的速查表,几乎覆盖了日常使用中最常踩的坑:
| 常见报错/现象 | 可能原因 | 解决方案 |
|---|---|---|
| selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary | 找不到Chrome浏览器安装路径 | 指定binary_location参数,或检查浏览器安装是否完整 |
| selenium.common.exceptions.SessionNotCreatedException: This version of ChromeDriver only supports Chrome version XX | 驱动和浏览器版本不匹配 | 使用Selenium Manager自动管理,或手动下载匹配版本的driver并配置环境变量 |
| NoSuchElementException | 元素定位表达式错误或元素不存在 | 核对元素属性,切换iframe,使用显式等待 |
| ElementNotInteractableException | 元素存在但不可操作,比如被遮挡或处于禁用状态 | 检查元素是否被其他元素遮盖,或用JS执行点击操作 |
| TimeoutException | 显式等待或隐式等待超时 | 检查网络延迟、元素是否动态加载、等待条件是否选择正确 |
| StaleElementReferenceException | 元素引用过期,页面刷新后原引用失效 | 重新获取元素对象,避免长时间持有元素引用 |
| InvalidSelectorException | XPath或CSS选择器语法错误 | 在浏览器控制台里验证表达式,使用规范的语法 |
| ElementClickInterceptedException | 目标元素被其他元素挡住,无法点击 | 等待遮挡元素消失,或者用ActionChains鼠标移动点击 |
| 脚本运行速度越来越慢 | 页面积累了大量dom节点,或存在死循环 | 检查页面是否有自动刷新机制,适当做页面清理,不要长时间不关闭浏览器 |
| 多个用例同时跑时相互干扰 | 浏览器实例冲突或全局状态污染 | 每个用例独立创建driver实例,测试数据隔离,文件锁防止并发写冲突 |
6.2 我在实战中总结的几个关键心得
分享几个我踩过多次坑之后才总结出来的心得。
第一个是关于元素定位的优先级。能用id就用id,id是HTML标准属性,基本上不会有歧义。id用不了再试css_selector,css的速度快、语法简洁,它处理class和属性值还有模糊匹配的能力,覆盖面很广。xpath放在最后用,因为它虽然最灵活,但也最容易写得臃肿,而且很多项目的HTML结构改动频繁,xpath写得太深入,一改页面就全崩了。
第二个是关于浏览器实例的管理。很多人为了省时间,让一个浏览器实例从头用到尾,跑完所有用例。这样看起来省了启动浏览器的几秒,实则隐患很大:浏览器长期运行会累积缓存和内存碎片,页面元素引用容易过期,用例之间的状态也会相互污染。我的做法是每个测试用例或者每个测试模块独立启动和关闭浏览器,虽然多花几秒,但换来的稳定性是值得的。
第三个非常关键的细节,页面元素枚举与定位元数据分离。早期我写自动化脚本,喜欢把每一个定位表达式直接散落在脚本里,结果是脚本越写越长,各种定位表达式四处开花,一旦前端改了某个按钮的class,就要在十几个文件里找来找去改一遍,每次都要头大。后来我就开始做一个专门存放定位元数据的模块,统一管理所有元素定位表达式,脚本里只引用模块中的变量。再后来更进一步,把定位元数据存到配置文件里,实现数据和代码彻底分离。这样做的好处非常明显,维护工作量显著下降,而且新同事接手项目时,只需要打开定位管理文件,就能快速了解页面上有哪些关键元素、是怎么定位的。
# 定位元数据管理文件 locators.py class LoginPageLocators: USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.ID, "password") LOGIN_BUTTON = (By.ID, "loginBtn") ERROR_MESSAGE = (By.CSS_SELECTOR, ".error-message") NICKNAME_TEXT = (By.XPATH, "//div[@class='user-info']//span[@class='nickname']")在测试脚本里,就可以这样使用这些定位元数据:
from locators import LoginPageLocators as loc login_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable(loc.LOGIN_BUTTON) ) login_btn.click()这种组织方式的优势,在项目规模稍微变大之后会体现得极为明显。定位元数据集中管理,不仅方便前端改动时快速搜索替换,也方便以后做跨浏览器运行时根据浏览器类型切换不同的定位策略,算是一个可以尽早养成的工程习惯。
第四个心得,是关于测试数据的管理。不要把测试数据硬编码在脚本里,比如登录的用户名密码,如果你直接写在代码里,之后要改测试环境或者换一组测试账号,又得一个个去改。我通常的做法是把测试数据写到独立的配置文件里,脚本运行时从配置读取。常用的有yaml、json、Excel,用yaml的比较多,因为它的结构清晰,支持注释,阅读起来也舒服:
# test_data.yaml login_data: correct_user: "test_user" correct_password: "Test123456" wrong_password: "wrong_password" expected_success_message: "test_user" expected_fail_message: "用户名或密码错误"脚本加载配置文件也很简单:
import yaml with open("test_data.yaml", "r", encoding="utf-8") as f: test_data = yaml.safe_load(f) login_data = test_data["login_data"]数据和代码分离以后,新增一个测试环境的账号密码,只需要更新配置文件即可,完全不需要动测试代码。前期的项目里,我还经常遇到UI文本变动的情况,比如错误提示文案从“用户名或密码错误”改成了“账号或密码错误”,这时候只要改配置文件,脚本完全不受影响。这个习惯看起来微小,实际上能节省大量后期维护成本。
7. 写好Selenium脚本的工程化意识
自动化测试写多了以后,我发现真正拉开两个人水平差距的,往往不是谁更熟悉API用法,而是谁更有工程化意识。所谓工程化意识,通俗来说,就是你写的自动化测试脚本,能不能让别人容易接手,能不能在项目迭代中持续稳定运行,能不能在出问题时快速定位。
第一个工程化习惯,是实现用例之间的独立性。每个测试用例都应该是一个独立的功能,不依赖其他用例的执行顺序,也不依赖测试数据在上一个用例中的状态。你想想,如果用例A修改了某个全局状态,用例B又依赖这个状态,那一旦单独跑B就必定失败。所以,写用例的时候,数据准备和清理动作,都应该做到用例内部闭环。
第二个工程化习惯,是合理设计日志记录。脚本运行过程中,要有足够的日志输出让我们知道执行到了哪一步。Python的logging模块比print更能胜任这个任务,它可以同时输出到控制台和文件,还可以设置不同的级别。生产级的自动化测试项目,日志记录几乎是标配。
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler("test_run.log", encoding="utf-8"), logging.StreamHandler() ] ) logging.info("开始执行登录测试用例:TC_001")第三个习惯,是主动留存失败现场。脚本运行失败时,自动截图保存到指定目录,这个操作的价值在排查问题时怎么强调都不过分。Selenium本身提供了截图接口,我们可以封装一下,让它和pytest的失败钩子关联起来:
import datetime def take_screenshot(driver, name): timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") file_path = f"./screenshots/{name}_{timestamp}.png" driver.get_screenshot_as_file(file_path) return file_path一旦一个用例在半夜定时任务里跑失败了,第二天早上打开测试报告,看到失败截图里页面的实际渲染状态,定位效率会成倍提升。否则,你只能凭一条干巴巴的报错信息,去猜当时页面上到底发生了什么。
第四个习惯,是编写测试用例的注释和说明文档。我知道很多技术人员都抗拒写文档,但在自动化测试这个领域,文档和注释的价值特别高。因为自动化测试用例动辄几十上百条,如果每条用例没有清晰的用途说明、前置条件和预期结果描述,时间一长,连自己写的人都可能忘了当初的思路。每个用例文件头部至少要说明:测的是哪个模块、依赖哪些数据、运行前提是什么。
这些工程化的做法,短期内可能看不出什么明显效果,但当你维护的自动化用例数量超过一百个、项目迭代超过三个月之后,它们的价值就会彻底显现出来。我见过太多团队,自动化测试初期进展很快,但没过多久用例就开始大量失败,最终整个项目废弃,核心原因往往不是技术问题,而是工程化意识不足,脚本没有形成可维护的结构。
自动化测试这条路,入门其实不难,但想做好做持久,拼的是细节和习惯。Selenium加Python,给了我一套非常趁手的工具,而真正让这套工具发挥价值的,是我们在使用过程中沉淀下来的方法论。希望这篇基于个人项目经验的文章,能帮你少踩一些坑,更快地跑通属于自己的第一条自动化用例。