资讯中心

Python+Selenium+unittest搭建UI自动化测试框架实践指南

📅 2026/10/12 12:33:23
Python+Selenium+unittest搭建UI自动化测试框架实践指南
1. 整体思路为什么是 python unittest html先说清楚一个核心观点UI 自动化测试框架不是越复杂越好而是越适合团队当前阶段越好。我刚接到这个任务时团队里没有专门的测试开发岗测试同学普遍只会写简单的 Python 脚本对 pytest、BDD、Allure 这套生态并不熟悉。如果一上来就引入 pytest requests Allure CI/CD 全链路学习成本和维护成本都很高落地周期会被拉得很长。所以我选择了一条“够用、好懂、快上手”的技术路线Python Selenium unittest HTMLTestRunner。这套组合的特点我用一句话概括Python 负责写逻辑Selenium 负责操作浏览器unittest 负责组织用例和断言HTMLTestRunner 负责把执行结果变成一份能直接发给领导和开发看的 HTML 报告。简单拆解一下每个组件的定位Python开发语言生态成熟第三方库丰富写起脚本来比 Java 少很多样板代码。SeleniumUI 自动化的事实标准支持 Chrome / Firefox / Edge 等主流浏览器能模拟用户的点击、输入、滚动、悬停等操作。unittestPython 标准库自带的测试框架不需要额外安装支持用例组织、批量执行、断言、fixturesetUp / tearDown等机制。HTMLTestRunner一个基于 unittest 结果生成 HTML 报告的三方扩展库。虽然官方原版还停留在 Python 2 时代但社区有适配 Python 3 的魔改版用起来完全没问题。这个组合适合的人群非常明确自动化测试从 0 到 1 阶段的测试工程师、想快速搭建一套可持续回归 UI 用例的技术团队、以及那些不想把框架搞成“玩具项目”但又没空研究复杂生态的务实派。接下来我会从框架设计、目录结构、核心代码、问题排查四个层面完整复盘这套框架的搭建过程。这篇文章不是教程式的堆叠而是一个踩过坑的人的实践总结很多细节不亲自动手是写不出来的。2. 框架设计与目录结构先把地基打稳2.1 设计原则分层、配置化、可维护很多新手搭 UI 自动化框架最大的问题不是不会写用例而是把用例和底层操作全部搅在一起。比如在用例里直接driver.find_element(...)、直接写time.sleep(5)、直接处理弹窗……这样写 10 条用例还行写到 50 条的时候一旦页面元素 ID 变了你得一个一个去改用例里的定位表达式维护成本直接爆炸。所以我在搭建这套框架时坚持了三个原则第一分层隔离。把用例、页面元素、公共操作、配置信息拆成不同模块。用例层只关心“做什么业务操作、断言什么结果”底层通过 Page Object 模式把元素定位和操作细节封装掉。这样页面变了动的只是对应页面类用例本身不受影响。第二配置驱动。浏览器类型、被测系统地址、超时时间、账号密码这些信息不硬编码在代码里而是统一放到配置文件config中。换环境、换浏览器改配置文件即可不用改代码。第三结果可视化。自动化测试跑了之后必须能生成直观、带截图、带错误信息的报告。HTMLTestRunner 在这里承担了“中间件”的角色它能把 unittest 的执行结果转换成带统计信息的网页报告。2.2 目录结构设计我最终采用的目录结构如下你可以根据自己的项目规模做删减ui_test_framework/ ├── config/ │ ├── __init__.py │ └── config.py # 全局配置URL、浏览器、超时、账号等 ├── common/ │ ├── __init__.py │ ├── logger.py # 日志封装 │ ├── screenshot.py # 失败截图处理 │ └── send_mail.py # 测试报告邮件发送 ├── pages/ │ ├── __init__.py │ ├── base_page.py # Page Object 基类封装 driver 公共操作 │ ├── login_page.py # 登录页对象 │ └── home_page.py # 首页对象 ├── testcases/ │ ├── __init__.py │ ├── test_login.py # 登录功能的测试用例 │ └── test_home.py # 首页相关测试用例 ├── test_data/ │ ├── __init__.py │ └── data.yaml # 测试数据可选用例少时可直接写在用例里 ├── reports/ │ └── (生成的 HTML 报告和截图会放这里) ├── logs/ │ └── (运行日志会放这里) ├── run_all.py # 测试执行入口 └── requirements.txt # 依赖清单这里我重点解释两个容易被忽略的设计细节一个是 pages 目录的存在意义。没有 pages 目录的框架用例里会到处是driver.find_element(By.ID, username).send_keys(admin)这种代码。有了 pages 目录业务页面被抽象成对象登录页长什么样、首页有哪些功能按钮都在对象内部描述。用例层只需要调用login_page.login(admin, 123456)就行。这就是 Page Object 模式的核心价值把“页面结构”和“业务用例”解耦。另一个是 common 目录下的公共模块。logger、screenshot、send_mail 这些不是锦上添花而是自动化测试跑起来之后的刚需。日志能帮你定位问题是在哪一步挂的截图能在报告里直观展示页面当时的状态邮件通知能让团队成员在早上看到昨晚回归的结果。2.3 依赖清单requirements.txt依赖文件看起来简单但版本锁定是个良心活。我的 requirements.txt 内容如下供参考selenium4.15.2 webdriver-manager4.0.1 PyYAML6.0.1很多教程会让你直接pip install selenium装最新版但我在实践中遇到过升级后 API 不兼容的情况。所以建议你固定大版本尤其是团队协作时依赖不一致会导致“我本地能跑你本地报错”的尴尬场面。注意HTMLTestRunner 默认不在 PyPI 官方源里需要单独下载源文件放到项目中。我一般创建一个common/HTMLTestRunnerCN.py或common/HTMLTestRunner.py的模块从国内社区下载适配 Python 3 的版本放进去。后面代码里会用from common import HTMLTestRunner这样的方式导入。另外推荐安装webdriver-manager它会自动帮你下载和管理对应浏览器的 driver 版本。省去手动下载 chromedriver 的痛苦尤其当你本地浏览器更新后 driver 版本不匹配时这个库能救你一把。3. 核心代码逐层实现从配置到报告一次跑通3.1 配置文件config/config.py配置模块是所有其他模块的地基。我把环境和账号信息都放在这里并且通过dict组织方便后续扩展多套环境测试环境、预发布环境。# -*- coding: utf-8 -*- import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 环境配置可切换 ENV test ENVIRONMENTS { test: { url: https://test.example.com, username: test_user, password: test_pass, }, pre: { url: https://pre.example.com, username: pre_user, password: pre_pass, }, } CURRENT_ENV ENVIRONMENTS[ENV] # Selenium 配置 BROWSER chrome # chrome / firefox / edge HEADLESS False # 是否无头模式本地调试时建议 False IMPLICITLY_WAIT 10 # 隐式等待单位秒 PAGE_LOAD_TIMEOUT 30 # 页面加载超时 EXPLICIT_WAIT 15 # 显式等待默认超时 # 报告和日志 BASE_URL CURRENT_ENV[url] REPORT_DIR os.path.join(BASE_DIR, reports) LOG_DIR os.path.join(BASE_DIR, logs) SCREENSHOT_DIR os.path.join(REPORT_DIR, screenshots) # 确保目录存在 for _dir in [REPORT_DIR, LOG_DIR, SCREENSHOT_DIR]: if not os.path.exists(_dir): os.makedirs(_dir)说一个配置细节隐式等待和显式等待不要混用成双重等待导致效率低下。隐式等待是全局的一旦设置所有find_element都会生效。显式等待是针对特定条件的比如等待某个元素可点击。我一般全局设一个 10 秒的隐式等待在需要精细化控制的地方再叠加显式等待。如果你把隐式等待设成 30 秒用例失败时每个元素定位都要等满 30 秒调试体验会非常差。3.2 日志模块common/logger.py日志在 UI 自动化里特别重要。因为用例跑失败时不能保证每次都有截图而日志记录了每一步操作的时间点和动作内容。我封装了一个轻量的 logger# -*- coding: utf-8 -*- import logging import os import time from config.config import LOG_DIR class Logger: 统一日志管理器 def __init__(self, nameui_test): self.logger logging.getLogger(name) self.logger.setLevel(logging.INFO) # 避免重复初始化 if not self.logger.handlers: fmt logging.Formatter(%(asctime)s - %(levelname)s - %(message)s) # 文件输出 log_file os.path.join(LOG_DIR, ftest_{time.strftime(%Y%m%d)}.log) fh logging.FileHandler(log_file, encodingutf-8) fh.setLevel(logging.INFO) fh.setFormatter(fmt) self.logger.addHandler(fh) # 控制台输出 ch logging.StreamHandler() ch.setLevel(logging.INFO) ch.setFormatter(fmt) self.logger.addHandler(ch) def get_logger(self): return self.logger log Logger().get_logger()我踩过的坑是如果Logger()被多个模块重复调用每次都会加 handler最终日志会重复打印好几遍。所以我在 init 里加了if not self.logger.handlers的判断。这个细节虽然不起眼但能让你的日志干净很多。3.3 基类页面封装pages/base_page.pybase_page.py 是整个框架的“底盘”所有页面对象都会继承它。这里我封装了 Selenium 的常用操作元素定位、点击、输入、等待、截图、滚动等。# -*- coding: utf-8 -*- import time import os from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.common.exceptions import TimeoutException, NoSuchElementException from config.config import EXPLICIT_WAIT, SCREENSHOT_DIR from common.logger import log class BasePage: def __init__(self, driver): self.driver driver def find_element(self, locator, timeoutEXPLICIT_WAIT): 显式等待并查找元素locator 格式为 (By.ID, username) try: element WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) return element except TimeoutException: log.error(f元素定位超时: {locator}) self.take_screenshot() raise def find_element_clickable(self, locator, timeoutEXPLICIT_WAIT): 查找可点击元素用于按钮等场景 try: element WebDriverWait(self.driver, timeout).until( EC.element_to_be_clickable(locator) ) return element except TimeoutException: log.error(f元素不可点击: {locator}) self.take_screenshot() raise def input_text(self, locator, text): 输入文本前先清空再输入避免残留内容 element self.find_element(locator) element.clear() element.send_keys(text) log.info(f输入文本: {text} - {locator}) def click(self, locator): 点击元素 element self.find_element_clickable(locator) element.click() log.info(f点击元素: {locator}) def take_screenshot(self, nameNone): 失败时截屏保存文件名为时间戳场景名 if name is None: name time.strftime(%Y%m%d_%H%M%S) file_path os.path.join(SCREENSHOT_DIR, f{name}.png) self.driver.save_screenshot(file_path) log.info(f截图保存: {file_path}) return file_path def switch_to_iframe(self, iframe_locator): 处理 iframe 场景 self.driver.switch_to.frame(self.find_element(iframe_locator)) log.info(f切换到 iframe: {iframe_locator})我特意在input_text里写了element.clear()因为很多输入框默认有 placeholder 或者上次输入的残留值不清空直接send_keys可能导致文本拼接。这个细节看似零碎但真实业务里出现率极高。另外截图逻辑必须放进公共方法中。这样一旦元素定位失败无论哪个页面对象都会自动截图不用在每条用例里单独处理。3.4 页面对象pages/login_page.py有了基类写页面对象就轻松多了。以登录页为例# -*- coding: utf-8 -*- from selenium.webdriver.common.by import By from pages.base_page import BasePage from common.logger import log class LoginPage(BasePage): 登录页面对象 # 页面元素定位统一集中在页面类顶部方便维护 username_input (By.ID, username) password_input (By.ID, password) login_button (By.ID, loginBtn) error_toast (By.CLASS_NAME, el-message) def login(self, username, password): 登录操作返回实例以便链式调用或断言 self.input_text(self.username_input, username) self.input_text(self.password_input, password) self.click(self.login_button) log.info(f用户 {username} 执行登录操作) return self def get_error_message(self): 获取登录失败时的错误提示 return self.find_element(self.error_toast).text这里使用了selenium.webdriver.common.by.By的元组定位方式。这种方式比直接传字符串更规范ide 还能自动补全减少低级拼写错误。关于页面对象要不要返回新页面的对象比如return HomePage(self.driver)我可以告诉你框架简单时别过度设计。返回self足够支撑大多数场景了。真要切换到新页面直接在用例里driver已经是最新页面状态不需要搞复杂的页面跳转关系。3.5 测试用例testcases/test_login.py用例层是最直观的一层也是测试同学最常写的部分。我强烈建议你遵守 unittest 的规范用例类继承unittest.TestCase方法名以test_开头。# -*- coding: utf-8 -*- import unittest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service from config.config import BASE_URL, BROWSER, HEADLESS from common.logger import log from pages.login_page import LoginPage class TestLogin(unittest.TestCase): 登录功能测试用例 classmethod def setUpClass(cls): 整个测试类只启动一次浏览器 log.info( 开始执行登录用例 ) options webdriver.ChromeOptions() if HEADLESS: options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) # webdriver-manager 自动匹配浏览器版本 service Service(ChromeDriverManager().install()) cls.driver webdriver.Chrome(serviceservice, optionsoptions) cls.driver.maximize_window() cls.driver.get(BASE_URL) cls.driver.implicitly_wait(10) classmethod def tearDownClass(cls): 测试类结束后关闭浏览器 cls.driver.quit() log.info( 登录用例执行结束 ) def test_login_success(self): 正常流程正确账号密码登录成功后跳转首页 login_page LoginPage(self.driver) login_page.login(test_user, test_pass) # 断言 URL 发生变化说明跳转成功 self.assertIn(home, self.driver.current_url, 登录成功后未跳转到首页) def test_login_failed_with_wrong_password(self): 异常流程密码错误时出现错误提示 login_page LoginPage(self.driver) login_page.login(test_user, wrong_pass) error_text login_page.get_error_message() self.assertIn(密码错误, error_text, f错误提示文案不符实际为: {error_text}) def test_login_with_empty_password(self): 边界场景密码为空时给出提示 login_page LoginPage(self.driver) login_page.login(test_user, ) error_text login_page.get_error_message() self.assertNotEqual(error_text, , 密码为空时未出现错误提示) if __name__ __main__: unittest.main()关于setUpClass和setUp的选择我说说自己的使用经验用例量少、且用例之间没有太强依赖时我倾向于使用 setUpClass 只启动一次浏览器能节省大量时间。如果用例量很多或者每条用例需要独立环境比如不同用户身份登录那就用setUp每个用例都启动新浏览器。这要看你业务场景具体决定没有绝对对错。不得不提的是assertIn(home, self.driver.current_url)这种断言。UI 自动化的断言不能只停留在“元素存在”层面要充分利用页面状态URL 变化、按钮可用性、标题变化、元素是否存在。这样用例才能更真实地反映业务预期。3.6 入口文件run_all.py这是框架的“总开关”作用包括收集所有用例、生成 HTML 报告、记录日志。# -*- coding: utf-8 -*- import unittest import time import os from common import HTMLTestRunner from config.config import REPORT_DIR def all_cases(): 加载 testcases 目录下所有测试用例 suite unittest.TestLoader().discover( start_diros.path.join(os.path.dirname(os.path.abspath(__file__)), testcases), patterntest_*.py, ) return suite def run(): suite all_cases() # 报告文件名带时间戳方便历史对比 report_name fui_test_report_{time.strftime(%Y%m%d_%H%M%S)}.html report_path os.path.join(REPORT_DIR, report_name) with open(report_path, wb) as fp: runner HTMLTestRunner.HTMLTestRunner( streamfp, titleUI 自动化测试报告, description测试环境: 测试环境 | 执行人: QA, verbosity2, ) runner.run(suite) print(f测试报告已生成: {report_path}) return report_path if __name__ __main__: run()unittest.TestLoader().discover()会自动扫描指定目录下所有符合pattern的文件把里面的TestCase收集到一个测试集里。这就是为什么我建议测试文件命名必须用test_开头否则 discover 会漏掉。关于 HTMLTestRunner 的导入方式不同版本差异很大。有的版本是from HTMLTestRunner import HTMLTestRunner有的是from HTMLTestRunnerCN import HTMLTestRunner。建议你先在本地写个空用例跑一遍确认报告能生成再继续。3.7 报告中的截图展示把失败证据嵌入报告默认 HTMLTestRunner 的失败信息只有文字 traceback看不到页面现场。为了更直观我在失败的时候会调用take_screenshot()并给addFailure加自定义逻辑。不过这里有个麻烦HTMLTestRunner 原生的addFailure方法不会自动嵌入截图。我的处理方式比较轻量在断言失败的地方通过self.failureException配合自定义函数把截图路径写入日志同时在报告里通过增加description在测试方法的 docstring 里写用例描述报告就会自动显示用例名称和 docstring。如果你想要截图直接嵌入 HTML 报告需要对 HTMLTestRunner 的TestResult类做定制我会在第 4 章里详细讲这个问题。3.8 邮件通知一条命令把报告发给团队自动化测试的价值在于持续回归而持续回归如果没有通知机制基本等于“黑盒”。我封装了一个 send_mail 模块执行完用例后自动把报告作为附件发送# -*- coding: utf-8 -*- import smtplib import os from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication SMTP_SERVER smtp.example.com SMTP_PORT 465 SENDER qaexample.com PASSWORD your_password RECEIVERS [devexample.com, pmexample.com] def send_report(report_path): msg MIMEMultipart() msg[From] SENDER msg[To] ,.join(RECEIVERS) msg[Subject] UI 自动化测试报告 body MIMEText(UI 自动化测试已执行完毕报告见附件。, _charsetutf-8) msg.attach(body) with open(report_path, rb) as f: part MIMEApplication(f.read()) part.add_header(Content-Disposition, attachment, filenameos.path.basename(report_path)) msg.attach(part) # 如果使用 465 端口用 SMTP_SSL如果使用 587用 SMTP starttls server smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) server.login(SENDER, PASSWORD) server.sendmail(SENDER, RECEIVERS, msg.as_string()) server.quit()邮件发送这个功能在真实团队里非常实用。不过要注意密码不要硬编码到代码里可以用环境变量或单独的配置文件管理避免把敏感信息推到代码仓库中。4. 实操过程与核心环节实现把框架跑起来4.1 环境准备Python 版本与依赖安装环境准备是很多新手第一个“拦路虎”。我的建议是使用 Python 3.8 及以上版本推荐 3.10。就这套框架来说Python 3.10 兼容性最好。建立虚拟环境也是一个好习惯。我见过很多同事在电脑上直接pip install结果不同项目依赖冲突最后要么升级破碎要么重启大法。推荐这样做# 创建虚拟环境 python -m venv venv # 激活Windows venv\Scripts\activate # 激活macOS / Linux source venv/bin/activate # 安装依赖 pip install -r requirements.txt依赖安装好后可以快速验证 Selenium 是否可用python -c from selenium import webdriver; print(selenium ok)4.2 浏览器驱动自动化管理webdriver-manager 的使用以前手动下载 chromedriver 是件烦人的事你还需要找到与自己 Chrome 版本完全匹配的驱动。后来我发现webdriver-manager这个库能自动检测 Chrome 版本并下载对应驱动。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)不过说句实话在公司的 CI 服务器上我一般会直接下载好驱动放到固定的目录用环境变量PATH指定避免每次都用 webdriver-manager 从网上下载占用时间。本地开发用 webdriver-manager 是真方便双管齐下。4.3 元素定位策略少用 xpath 绝对路径UI 自动化的头号困难就是元素定位。我的经验排序是优先使用id其次是name、class name再是css selector最后才考虑xpath尤其不要用绝对路径xpath看起来万能但页面结构调整时极容易碎。//*[idapp]/div[1]/div[2]/div[1]/form/input这种绝对路径只要多套一层 div 就全挂。我通常只在确实没有好的属性可用时才写 xpath而且尽量用相对定位# 推荐通过文本定位按钮 (By.XPATH, //button[contains(text(), 登录)]) # 推荐通过 placeholder 属性定位输入框 (By.XPATH, //input[placeholder请输入用户名])4.4 等待策略远离 time.sleep新手最喜欢写time.sleep(5)好像不 sleep 页面就加载不出来。但 sleep 的问题很严重固定时间等待页面快了浪费时间页面慢了直接报错。正确做法是使用 Selenium 的显式等待WebDriverWait配合 expected_conditionsfrom selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, username)) )这样做的效率远高于 sleep元素一出现立刻继续执行最长只等设定的 10 秒。后面我在 BasePage 里封装的find_element就集成了这一套逻辑所以页面对象里不要再去写driver.find_element_by_id这样的裸调用。4.5 用例执行顺序unittest 默认并非文件顺序有个坑必须提醒你unittest 的用例执行顺序不是定义顺序而是按方法名的 ASCII 码排序。比如你定义了def test_login_success(self): # 先执行 def test_logout(self): # 后执行实际上 unittest 可能会先执行test_login_success再执行test_logout因为 l 在 o 前面。如果你有明确的顺序依赖建议用例之间保持独立或者通过命名前缀控制执行顺序test_01_login、test_02_logout。但我的建议是用例一定要独立不要有顺序依赖。自动化最怕的是“前一条影响后一条”排查问题翻车率极高。4.6 HTMLTestRunner 报告的深度定制很多网络上流传的 HTMLTestRunner 版本界面比较粗糙。我看了一下我手里这个版本其实可以做得更精致一点在某些版本里你可以通过修改HTMLTestRunner.py里的 CSS 来调整报告样式。但比起样式我更推荐关注三大核心功能失败用例的 Traceback 展示报告默认会显示失败详情这非常重要能让人一眼看出断言失败原因。测试用例的分组和统计报告顶部通常有通过/失败/错误数量的统计饼图或数字适合在汇报时直接展示。持续集成的存档报告中带上时间戳文件名方便和上一轮结果做 diff看回归趋势。我一般还会通过unittest.TestSuite把冒烟用例单独分出跑一个快速子集def smoke_suite(): 只跑冒烟用例 suite unittest.TestSuite() suite.addTest(TestLogin(test_login_success)) return suite这样在每天快速回归前可以先跑冒烟用例挂了就报警剩下的大批量用例没必要浪费时间。4.7 多浏览器的执行策略框架如果只支持一种浏览器说服力就打折了。所以我习惯把浏览器参数化webdriver.Chrome() webdriver.Firefox() webdriver.Edge()在 config.py 里加一个BROWSER变量然后根据变量的值选择不同 driverdef get_driver(browser_name): if browser_name chrome: return webdriver.Chrome(...) elif browser_name firefox: return webdriver.Firefox(...) elif browser_name edge: return webdriver.Edge(...) else: raise ValueError(f不支持的浏览器: {browser_name})实测下来Chrome 和 Edge 的兼容性最好Firefox 偶尔会遇到关闭浏览器时 driver 进程不释放的坑。5. 常见问题与排查技巧实录5.1 元素定位超时明明看得到但脚本说找不到这是 UI 自动化最典型的问题。我的排查顺序是看截图先打开失败自动截图确认页面是否停留在预期位置。看等待条件如果页面有异步加载AJAX需要增加显式等待比如等待某个元素出现、等待按钮可点击。检查 iframe如果元素在 iframe 里直接 find 是找不到的需要先switch_to.frame()。检查窗口/标签页如果点击后弹出了新窗口要switch_to.window()切换句柄再定位元素。检查元素属性是否动态变化很多前端框架比如 React / Vue的 class 是动态生成的比如classel-input el-input--medium。这种用 class 定位很容易挂改用固定的id或>options webdriver.ChromeOptions() options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False)但请注意这类反爬反自动化手段会持续升级遇到较强的风控系统时最可靠的方式是跟开发沟通开放测试后门或者使用企业内部的测试专用账号。不要去研究绕过风控的“黑科技”投入产出比太低容易走歪。5.3 报告样式错乱HTMLTestRunner 与 Python3 的兼容问题原版 HTMLTestRunner 是 Python 2 时代的作品直接用到 Python 3 会报错或报告样式异常。解决路径有两条使用开源社区维护好的 Python 3 版本直接下载放到项目里。如果只是小问题打开HTMLTestRunner.py源码看报错的具体行通常是把import StringIO改成from io import StringIO、把sys.stdout.write这类改掉就行了。我这边已经处理过这类问题如果你用的是我给的目录结构把HTMLTestRunner.py放到common目录下后在run_all.py中from common import HTMLTestRunner就能正常工作了。5.4 中文乱码问题报告全是问号报告里中文乱码一般是因为文件编码问题。解决方法是生成报告时以wb模式打开文件并在标题/描述中写中文源码文件头部统一加# -*- coding: utf-8 -*-如果还乱码可能是 HTMLTestRunner 源码中写死了某些编码检查有没有io.open之类的写法并指定encodingutf-8。5.5 用例之间的耦合影响driver 被意外关闭我在测试早期遇到过一个问题一条用例因为某个异常导致driver被关闭后续所有用例全部报错。排查下来发现问题主要出在有些用例的 finally 块中调用了driver.quit()而其他用例还在用同一个 driver。解决方案driver 的创建和销毁只能发生在setUpClass/tearDownClass中用例内部不要随意 quit。如果要隔离浏览器实例就使用setUp/tearDown让每个用例独立驱动而不是在用例中间夹杂quit和重新get的逻辑。5.6 CI 流水线中跑 UI 自动化需要无头模式本地跑能过上了 CI 服务器死活不行。最常见原因是CI 服务器是 Linux 系统没有图形界面。解决办法就是在 get_driver 里根据环境变量打开无头模式HEADLESS os.environ.get(HEADLESS, false) true if HEADLESS: options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080)--no-sandbox和--disable-dev-shm-usage在容器环境里几乎必备否则 Chrome 会报诡异的异常。5.7 元素点击失败明明元素存在点击却无效这种问题也特别多。原因可能是元素被其他 div 遮住了Selenium 的 click 会先做可见性检查如果元素不可见或不可点击会抛ElementClickInterceptedException。元素在页面的可视区域之外需要先driver.execute_script(arguments[0].scrollIntoView();, element)滚动过去。页面有 loading 遮罩层没消失按钮虽然存在但被 loader 挡了。我的处理办法是在 BasePage 的 click 方法里增加一个“滚动到元素 强制点击”的 fallbackdef click(self, locator): try: element self.find_element_clickable(locator) element.click() except Exception: element self.find_element(locator) self.driver.execute_script(arguments[0].scrollIntoView();, element) self.driver.execute_script(arguments[0].click();, element)这个技巧能解决很多“能看到但点不动”的怪问题。当然这只是应急手段真正的根因是前端页面可能渲染延迟加入显式等待比强点更好。6. 经验总结这套框架还能往哪走框架跑起来只是第一步真正让它产生价值的是持续迭代。根据我实际操作中的体会这套 python unittest html 组合在支撑中小规模 UI 自动化回归时完全够用而且因为结构简单、依赖少团队成员接手门槛很低。如果后续项目规模变大可以考虑几个方向引入数据驱动把测试数据抽到 Excel 或 YAML 中用ddt或parameterized库参数化用例减少大量重复的用例方法。往 pytest 迁移pytest 的 fixture 机制和插件生态确实更强如果你有精力可以逐步迁移但前提是团队有共识不要中途反复横跳。结合 Allure 报告Allure 能生成更美观、交互更强的报告还支持历史趋势。如果你经常向管理层汇报Allure 值得换。接入 Jenkins / GitLab CI把python run_all.py当成一个 CI 任务定时触发报告自动归档这是自动化测试发挥持续回归价值的核心一步。最后再分享一个我在实际使用中的体会UI 自动化测试的价值不在于用例数量多而在于稳定、可信、可持续。我见过很多人堆了几百条用例结果每天跑完一大半在报错开发已经免疫了报告看到红灯根本不慌。真正好用的框架是一方面用例设计得干净稳定另一方面能快速定位问题。这套框架的打法就是为此设计的。你按这个思路落地至少不会走得偏。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案