资讯详情

基于Selenium的个人博客系统UI自动化测试实践

📅 2026/10/20 8:43:29 | 华诺云谱 👁 阅读
基于Selenium的个人博客系统UI自动化测试实践
1. 项目概述与测试目标拆解1.1 为什么拿个人博客系统来练手这个项目的标题很明确基于 Selenium 的个人博客系统 UI 自动化测试实践。说白了就是用 Selenium 这套工具给一个个人博客系统做界面层面的自动化测试。我在接手这个项目时第一反应不是急着写代码而是先想明白一件事为什么偏偏选个人博客系统来当被测对象个人博客系统看起来简单其实五脏俱全。它有用户登录、文章发布、草稿保存、分类管理、标签筛选、全文搜索、评论互动、分页展示、后台管理这些典型功能几乎覆盖了 Web 应用最常见的交互形态。对有状态页面、有表单提交、有异步加载、有权限控制这样的系统拿来练 UI 自动化能踩到足够多的坑又不会像大型电商系统那样复杂到让人无从下手。对刚接触自动化测试的人来说这是性价比极高的练习靶场对已经有一定经验的测试工程师来说博客系统也够用来验证自己的框架设计思路。我当时的做法是先自己搭了一个轻量级的博客系统技术栈用的是 Spring Boot 加 Thymeleaf数据库用的 H2 内存数据库。为什么不用线上现成的博客平台来测原因很简单UI 自动化测试需要可控的环境。你要能随时重置数据、控制网络延迟、修改页面结构甚至故意制造一些异常状态来验证脚本的健壮性。外部平台你动不了数据也容易被别人污染跑一次挂一次根本没法稳定复现问题。所以自建被测系统是这类实践项目最稳妥的开局方式。1.2 测试范围划分和预期收益确定系统之后我给自己划了一条清晰的测试边界。UI 自动化测试不是什么都测它的目标非常聚焦验证用户通过界面操作能拿到预期结果这一层。至于后端接口的逻辑、数据库读写性能、缓存策略对不对那是接口测试和单元测试的事不归 UI 自动化管。我把博客系统的测试范围拆成了五块用户体系注册、登录、退出、会话失效后的跳转以及密码错误时的提示信息。文章管理发布新文章、编辑已有文章、删除文章、草稿保存、文章列表的分页展示。前台展示首页文章流渲染、文章详情页内容完整性、分类和标签的过滤结果。互动模块发表评论、评论列表刷新、评论后文章评论数是否变化。搜索功能按关键词搜索、空结果搜索、特殊字符搜索、搜索结果跳转详情页。每块业务都配上对应的正向用例和反向用例。比如登录模块正向是正确账号密码能进后台反向是错误密码要弹出用户名或密码错误的提示而且页面不能跳转。这些在手工测试里看起来稀松平常但自动化脚本跑起来之后价值就出来了每改一次前端代码我就把回归脚本跑一遍二十分钟能覆盖过去半天的手工工作量而且不会因为手滑漏点某个页签。在项目管理上我给这个实践定了三个成果指标。第一脚本要能无人值守地跑完整个回归流程失败时自动截图并输出日志第二用例要可以参数化比如用三组不同账号分别跑登录流程不需要复制三份脚本第三测试报告要能直接看出问题出在哪个页面、哪个步骤。这三个指标其实对应了自动化测试工程化的三个基本要求稳定性、可维护性、可读性。后面整个项目的技术选型和代码组织都是围绕这三条来走的。2. 技术选型与框架设计2.1 语言和测试框架怎么搭配Selenium 本身支持 Java、Python、C#、JavaScript 等多种语言我选了 Python理由很实际这个项目的定位是实践和快速迭代Python 写起来短平快同样是写一个页面对象的封装Java 可能要二十几行带各种 getter/setterPython 五六行就搞定了而且 pytest 的参数化、断言、固件机制用起来很顺手。测试框架层面我用了 pytest 加 pytest-html 的组合。pytest 的 fixture 机制可以用来管理浏览器实例的创建和销毁每个测试用例跑完自动关闭浏览器避免几百条用例把系统资源拖垮。pytest-html 用来生成带失败截图的 HTML 测试报告比默认的控制台输出直观得多。浏览器方面用的是 Chrome 对应的 WebDriver。为什么不用 Firefox没有特别深的原因就是 Chrome 的开发者工具看着习惯调试元素定位的时候直接右键复制 XPath 或者复制完整选择器效率高。如果你用的是 Firefox或者公司环境只允许用某种浏览器也没关系Selenium 这套代码几乎不用改只要换一个 Driver 实现就行。依赖管理直接建了一个 requirements.txt核心就几样selenium4.11.2 pytest7.4.0 pytest-html4.0.2 webdriver-manager4.0.1webdriver-manager 这个库强烈建议加上。它能自动匹配浏览器版本并下载对应的驱动省去了手动去下载 Driver 然后配置环境变量的步骤。早期做自动化的人多半都经历过Chrome 一升级脚本全挂的噩梦用这个库之后这种情况基本不会再发生了。2.2 Page Object 模式到底解决了什么接下来是框架设计。我在这个项目里采用了 Page Object Model翻译过来叫页面对象模型。这个模式我建议所有做 UI 自动化的人都认真学一下它是把脚本从能跑提升到能维护的关键一步。在不用 Page Object 的写法里你的测试代码会长这样def test_login(): driver.find_element(By.ID, username).send_keys(tester) driver.find_element(By.ID, password).send_keys(123456) driver.find_element(By.ID, loginBtn).click() assert 后台管理 in driver.page_source看起来没什么问题但假设登录页的输入框 ID 从username改成了user_login你得把所有测试用例里用到这个 ID 的地方全部找出来改一遍。如果登录操作被五个测试用例引用就是五处修改定位器一变整个项目跟着抖。这就是典型的改了前端一个属性测试脚本全崩。Page Object 的做法是把页面抽象成一个类元素定位和操作动作都封装在类里面class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.ID, loginBtn) def input_username(self, username): self.driver.find_element(*self.username_input).send_keys(username) def input_password(self, password): self.driver.find_element(*self.password_input).send_keys(password) def click_login(self): self.driver.find_element(*self.login_button).click() def login(self, username, password): self.input_username(username) self.input_password(password) self.click_login()然后测试用例就变成了def test_login_success(page_fixture): login_page LoginPage(page_fixture.driver) login_page.login(tester, 123456) assert page_fixture.driver.current_url.endswith(/admin/dashboard)这样一改前端如果改了元素定位你只需要改 LoginPage 这一个类所有依赖它的用例全部自动适配。而且测试用例读起来像散文一眼就知道它在验证什么行为而不是一堆 find_element 的堆砌。2.3 目录结构和 fixture 设计聊完模式看一下实际项目的目录长什么样。我习惯把代码按页面对象、测试用例、工具函数、测试数据、报告输出这五个维度分开互不纠缠blog_auto_test/ ├── pages/ │ ├── __init__.py │ ├── base_page.py │ ├── login_page.py │ ├── article_admin_page.py │ ├── blog_home_page.py │ └── article_detail_page.py ├── testcases/ │ ├── __init__.py │ ├── conftest.py │ ├── test_login.py │ ├── test_article_manage.py │ ├── test_comment.py │ └── test_search.py ├── utils/ │ ├── __init__.py │ ├── screenshot.py │ ├── log_config.py │ └── data_loader.py ├── testdata/ │ ├── users.json │ └── articles.json ├── reports/ │ └── (存放HTML测试报告和截图) ├── requirements.txt └── pytest.iniconftest.py 是 pytest 里非常重要的一个文件它专门用来声明 fixture。我在里面定义了一个page_fixture负责启动浏览器、隐式等待设置、用例结束后的清理工作import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager pytest.fixture(scopefunction) def page_fixture(): driver webdriver.Chrome(ChromeDriverManager().install()) driver.implicitly_wait(5) driver.maximize_window() yield driver driver.quit()注意 scope 设成了 function意思是每个测试用例都用一个全新的浏览器实例。这样可以保证用例之间互不污染代价是跑大批量用例时速度会慢一些。对于这个项目来说稳定性优先于速度所以 function 级是最合适的。如果以后用例量大了可以再改成 class 或者 module 级别来提升效率。3. 核心定位策略与关键实现3.1 页面元素定位ID 优先XPath 兜底元素定位是 UI 自动化的基本功也是出现失败最多的环节。我给自己定了一条定位优先级原则能用 ID 用 ID没有 ID 再用 name 或 CSS 选择器最后才考虑 XPath。为什么 ID 优先级最高因为它是页面里唯一一个可以做到全站唯一的属性。ID 的定位速度最快而且语义最稳定。我在前端开发博客系统时给每个关键交互元素都加了带语义化前缀的 ID比如登录按钮是loginBtn文章标题输入框是articleTitle提交按钮是publishBtn。这是从测试角度反推前端设计实际项目里前端不一定配合得这么好但对自己可控的练习项目刻意这样做能省掉大量维护成本。XPath 作为兜底方案重点掌握两种用法。一是通过文本定位元素比如页面上只有一个确认删除的按钮delete_btn (By.XPATH, //button[contains(text(), 确认删除)])二是通过层级关系定位比如文章列表里某一篇文章的编辑按钮可以先用文章的标题定位到那一行再向下找按钮edit_btn (By.XPATH, //h2[classarticle-title and contains(text(), Selenium实践指南)]/parent::div//a[contains(text(), 编辑)])这里用parent::轴从标题节点向上跳到包裹它的 div再向下找编辑链接比直接写一大串绝对路径要抗变化得多。3.2 显式等待UI 测试最值钱的半小时投入另一个核心点是等待策略。Web 应用现在普遍有异步渲染你点击了发布按钮前端要发请求、等后端写入、刷新列表这个过程可能要一秒钟也可能要三秒钟。如果在元素还没渲染出来时就强行执行下一步脚本立刻报错 NoSuchElementException。Selenium 提供了三种等待方式。固定休眠time.sleep()虽然简单但千万不能滥用它的问题是时间设短了慢机器上照样元素没出来时间设长了快机器上白白等了四五秒。一条用例等三五处整个回归时间被拖长好几倍。隐式等待driver.implicitly_wait(5)是全局的但它的机制是轮询 DOM 直到元素出现超过 5 秒才抛异常解决的是元素在不在的问题。显式等待WebDriverWait能解决更复杂的问题元素可见、可点击、文本变化、元素消失。我在项目里用了 WebDriverWait 的 expected_conditions 封装直接写在 base_page 里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 wait_until_visible(self, locator): return self.wait.until(EC.visibility_of_element_located(locator)) def wait_until_clickable(self, locator): return self.wait.until(EC.element_to_be_clickable(locator))这样在页面对象里任何关键操作之前都能先确认元素状态。比如发布文章后列表刷新可能需要时间脚本就等新文章标题先出现再断言发布成功的提示一步都不会抢跑。3.3 数据驱动一套脚本跑多组输入数据驱动是自动化测试里一个高频词它解决的问题很纯粹测试步骤是一样的只是输入数据不同。登录用例是最典型的例子。我建了一个 testdata/users.json{ valid_user: {username: tester, password: 123456, expect: success}, wrong_password: {username: tester, password: wrong, expect: fail}, empty_username: {username: , password: 123456, expect: fail} }然后在 pytest 里用pytest.mark.parametrize把数据注入到测试用例中import pytest import json from pages.login_page import LoginPage with open(testdata/users.json, encodingutf-8) as f: users json.load(f) pytest.mark.parametrize(case_data, users.values(), idsusers.keys()) def test_login_with_multiple_users(case_data, page_fixture): login_page LoginPage(page_fixture.driver) login_page.open(/login) login_page.login(case_data[username], case_data[password]) if case_data[expect] success: assert page_fixture.driver.current_url.endswith(/admin/dashboard) else: assert 用户名或密码错误 in page_fixture.driver.page_source三条用例共用一份脚本数据和步骤彻底分离。以后要加密码长度超限的用例只需要在 JSON 里加一条记录。这种组织方式的好处是测试数据谁都能改不需要动 Python 代码大大降低了维护门槛。4. 实操过程从环境搭建到全流程跑通4.1 环境搭建阶段的三件事先说环境搭建。不管你是 Windows、macOS 还是 LinuxPython 环境装好之后就三件事装依赖、装浏览器、跑通了就行。第一步在项目根目录建虚拟环境python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt第二步确认 Chrome 版本和 Driver 版本匹配。前面提到的 webdriver-manager 会自动完成匹配但如果你公司内网环境不允许自动下载就得手动去下载对应版本的 ChromeDriver然后把可执行文件放到系统 PATH或者通过webdriver.Chrome(executable_path/path/to/chromedriver)指定路径。第三步是验证环境。写一个最基础的小脚本打开一个页面打印标题如果这个能过说明 Selenium 和浏览器的链路已经通了from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from time import sleep driver webdriver.Chrome(ChromeDriverManager().install()) driver.get(http://localhost:8080/) print(driver.title) sleep(2) driver.quit()这一步千万别跳过很多人后面案例写好了却发现浏览器起不来回头排查才发现是自己把驱动版本搞错了。4.2 登录模块从用例设计到断言细节登录是博客系统的入口也是 UI 自动化最经典的练兵场。我在这部分花了比较多心思因为它的交互最简单但细节最多。页面对象 LoginPage 里我把三个操作封装成一个login()方法但内部每一步都做了等待。用户名输入框要等可见密码输入框要等可编辑登录按钮要等可点击。为什么不能等页面加载完直接输入因为现在很多页面是懒加载的整个登录区域可能先渲染了一个壳里面的输入框是异步填充进来的直接输入就会报错。登录成功的断言我做了两层。第一层断言 URL 发生了变化因为后端是 Spring Security 框架登录成功后会重定向到/admin/dashboard第二层是页面上必须出现后台管理特有的元素比如侧边栏的文章管理菜单。两层断言合在一起既验证了跳转也验证了页面内容渲染到位。如果只断 URL 不断内容万一后端重定向了但前端 JS 渲染崩了页面白屏你说这个用例算过还是不过登录失败的断言也有讲究。错误的用户名和密码组合一定要验证页面上的错误提示文案而不是简单地看 URL 没变。有些前端会把密码错误和用户不存在的提示分成两种文案这对用户是好事对你测试来说也可以当成两个用例来验证。4.3 文章发布与列表分页测试文章发布是博客系统的核心业务整个流程是进入后台、点击新建文章、填写标题和正文、选分类、打标签、点击发布、回到列表页确认文章出现。这里最容易出问题的是富文本编辑器。很多博客系统的正文编辑器是嵌入的 iframe普通方式定位正文输入框根本找不到元素因为焦点在 iframe 的 document 里而 WebDriver 默认在父级文档中查找。所以封装的时候必须先切换进去editor_frame driver.find_element(By.TAG_NAME, iframe) driver.switch_to.frame(editor_frame) editor_body driver.find_element(By.ID, editorContent) editor_body.send_keys(这是一篇自动化发布的测试文章) driver.switch_to.default_content()这个坑我专门记了一条心得所有用到 iframe 的场景记住进去之后一定要出来否则后续操作全在 iframe 里面找元素必挂。发布成功之后下一步是列表分页验证。我的博客系统设置的是每页显示 5 篇文章分页测试的用例是发布第 6 篇文章后应该翻到第二页才能看到它。这个场景验证的是新文章默认状态是否被正确推进列表以及分页组件的当前页状态是否正确。4.4 搜索和评论交互场景搜索功能的自动化需要特别注意空结果和特殊字符这两类输入。我在搜索框输入一个不存在的关键词断言页面上出现未找到相关文章的提示。特殊字符我输入了英文半角引号和百分号验证系统不会因为转义问题直接抛 500页面还能正常渲染。评论交互的自动化流程是在前台文章详情页找到评论输入框填写昵称和评论内容点击提交等待评论出现在列表里再验证评论数从 3 变成 4。这个流程的时间点卡得非常紧要点在于等待评论出现这个动作。我用显式等待去等新评论的文本元素出现而不是等固定时间既稳又不会拖慢整体速度。5. 测试报告与持续集成接入5.1 失败截图报告里最救命的信息没有截图的测试报告价值要打七折。用例失败时报错信息只能告诉你在哪一行发生了什么异常但页面当时到底长什么样你可能完全不知道。加了截图问题一目了然。我在 conftest.py 里加了 pytest 的钩子函数失败时自动截图pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(page_fixture) if driver: screenshot_path freports/screenshots/{item.name}_{int(time.time())}.png driver.save_screenshot(screenshot_path) report.screenshot_path screenshot_path执行完所有用例后再用 pytest-html 生成报告pytest testcases --htmlreports/blog_test_report.html --self-contained-html生成的报告里会列出每条用例的执行结果、耗时、失败原因以及失败的截图路径。我通常会把截图嵌入到报告里配合 pytest_html 的报告扩展来用。这样一份报告给别人看对方不需要打开终端自己去复现扫一眼截图就能定位问题特别适合配合开发人员做调试。5.2 接入 CI让测试跑在你没空的时候自动化测试和持续集成是天然搭档。我在项目里接入了本地的定时任务。因为博客系统跑在本机不需要太复杂的分布式环境我拿系统自带的任务调度器做了个简单的 CI 雏形。在 Windows 环境下可以利用任务计划程序设一个每天凌晨两点执行的批处理脚本。脚本先拉取最新前端代码重启博客服务然后跑 pytest最后把报告推到一个固定目录。第二天早上到公司直接打开报告看结果哪条用例挂了、挂了多久、截图是什么样清清楚楚。这听起来像是在本机模拟了 CI但对于个人博客这种轻量级项目已经足够验证自动化测试如何嵌入到开发流程中这件事。如果你的业务已经上了代码托管平台也可以用平台的 CI 功能在流水线里加一步跑测试命令效果是一样的环境更干净。6. 常见问题与排查技巧实录6.1 NoSuchElementException先分清楚是定位问题还是时机问题这个异常是 UI 自动化里出现频率最高的问题。它的常规报错信息是 Message: no such element: Unable to locate element。很多人一看到这个就急着改定位器其实忽略了另一个高频原因元素还没渲染出来脚本就跑过去了。我的排查套路是三步走。第一步看截图报告里如果有失败截图先看截图里页面上有没有你要找的元素。如果有说明是等待策略不够如果没有说明是定位策略问题。第二步看控制台日志Selenium 在抛出 NoSuchElementException 之前会先去查 DOM查了几次、每次间隔多少都会在日志里体现能帮你判断元素是始终不存在还是晚了一会儿才出现。第三步是打开浏览器开发者工具在 Console 里手动执行一下document.querySelector或者$x()来确认定位表达式到底能不能找到元素。如果确认是定位器的问题优先考虑能不能用更稳定的属性。比如按钮上的文本页面里的发布文章可能不止出现在一个地方侧边栏菜单也有一个同名的链接这时就要用层级关系去缩小范围。如果确认是时机问题把time.sleep()换成显式等待这是治本的办法。6.2 StaleElementReferenceException元素还在引用已经过期了这是我个人觉得最坑的一个异常。字面意思是元素引用过期。场景是这样的脚本先找到了文章列表第一行的标题元素存到一个变量里接下来进行了一个翻页或者刷新操作再回头操作这个变量结果元素已经被重新渲染过了旧的引用指向了一个幽灵节点操作它就报错。解决办法有两个。第一种是每次操作之前重新查找元素不要复用老的引用第二种是在页面对象里把查找动作封装成方法每次调用都重新执行find_element。我在项目里默认采用第二种因为这个方案对代码的侵入最小而且不容易遗忘。6.3 弹窗与 iframe 切换全局的焦点管理弹窗方面Selenium 的switch_to.alert可以处理浏览器原生弹窗包括 Alert、Confirm、Prompt 三类。但我必须提醒一句前端项目里大部分弹窗是模拟弹窗就是页面自己渲染出来的 div并不是浏览器原生弹窗。模拟弹窗的处理逻辑和普通元素一样等待它 visible 再点击关闭按钮就行。写脚本前先确认弹窗类型否则会在 switch_to.alert 上卡半天。iframe 切换我在前面文章发布部分已经讲过一个例子。这里再补充一个细节如果页面上有多个 iframeswitch_to.frame的参数可以用元素对象、id、也可以使用索引。最稳妥的是传元素对象先定位到要进入的那个 iframe再切换。退出时switch_to.default_content()回到主文档如果有多层嵌套switch_to.parent_frame()只退上一层。6.4 滚动加载与异步渲染让脚本等人不让人等脚本个人博客系统的前台页面我做了下拉加载更多的功能这给自动化测试带来了两类问题一是页面高度不够时目标元素虽然在 DOM 里但位置在视口之外Selenium 的 click 默认要先滚动到元素可见才能执行二是懒加载的内容只有在滚动到一定位置后才请求后端元素出现的时间完全取决于滚动位置和接口响应速度。我的处理方案是这两步的组合。第一步用一个通用的滚动工具方法def scroll_into_view(self, locator): element self.wait_until_visible(locator) self.driver.execute_script(arguments[0].scrollIntoView({behavior: instant, block: center});, element) return element第二步在滚动之后不急着断言先等后续加载的容器类元素出现。比如页面底部有一个加载完成的占位节点滚动到它附近后显式等待这个节点消失再开始操作下面的内容。这样既兼容了不同设备的屏幕高度又不会因为网络慢导致误报。6.5 测试数据管理与环境隔离让脚本每次都有干净的起点最后聊一个很多人到后期才意识到的问题测试数据的污染。UI 自动化测试是用真实接口、真实数据库的跑一次就会在系统里留下痕迹。比如你每跑一次发布文章的用例数据库里就多一篇文章。如果发布脚本被来回跑了十遍列表页里可能就有十篇自动化测试文章堆在那里分页的数量变了排序规则一变后续用例的断言就可能失败。我在项目里做了三件事来控制这个问题。第一在 pytest 的 fixture 里写了数据清理逻辑测试结束后把本次创建的测试文章从数据库里删掉。因为博客系统是我自己搭的可以直接调用一个自定义的管理接口来操作数据。第二用一组专门的前缀区分测试数据和真实数据比如文章标题统一带[AUTOTEST]前缀排查的时候一眼就能认出来。第三测试环境用独立数据库和生产数据彻底隔离。这样就算清理逻辑出了漏网之鱼删掉重来也不影响正常使用。这三点做完之后脚本的稳定性显著上升。最直观的变化是之前跑到第 47 条用例偶尔会挂重新跑一遍可能又过了这就是典型的脏数据导致的不稳定。现在每个用例的起点都是可预期的问题更容易复现也更容易定位。整个项目从设计到落地核心的心得其实可以浓缩成一句话UI 自动化测试不是写脚本的技术而是管理环境、管理数据、管理等待的技术。脚本谁都能写但能把脚本维持在一个今天能跑通、明天也能跑通、前端的按钮改了个样式也不至于全盘崩溃的状态才是真正有价值的能力。我建议你拿到任何练习项目都按这个思路走一遍先明确测试边界再用 Page Object 组织代码显式等待管好异步渲染数据驱动降低维护成本最后把报告和 CI 接上。走完这一圈你对 UI 自动化测试的理解会比单纯看文档深刻得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑