做UI自动化这事,我一开始是拒绝的。不光是我,团队里大多数测试同学听到“自动化”三个字,第一反应都是:稳定吗?值得吗?投入产出比会不会很惨?但真正把一套基于python+unittest+html的UI自动化测试框架从零跑到稳定之后,我才发现,这事没那么玄乎,也没那么难。它的核心门槛不在“自动化”本身,而在于你愿不愿意先把地基打好。
这套框架说白了就三层东西:python负责写逻辑,unittest负责组织和管理用例,html负责输出让人看得懂、领导也看得懂的报告。没有花哨的框架,没有复杂的平台依赖,每个人都可以在自己的电脑上从零搭起来。这篇文章我想把从环境搭建、目录设计、核心封装到报告生成、持续集成、踩坑记录的完整过程都写出来,适合刚接触自动化测试的测试工程师,也适合想自己搭一套轻量级框架的小团队参考。
1. 项目概述与技术选型思考
1.1 为什么选python+unittest+html而不是别的组合
现在的自动化测试工具和框架五花八门,商业的有UFT(就是以前的QTP),开源的有Robot Framework、pytest、Cypress、Playwright等等。但我最后选了python+unittest+html这套组合,核心原因就三个字:够轻、够稳、够通用。
python在测试圈的普及率太高了。哪怕你没系统学过python,只要会写一点脚本,看看selenium的API,几天时间就能上手写用例。而且python的库生态非常完善,除了selenium,后面想接requests做接口测试、接pytest做更复杂的用例管理、接allure生成报告,都是顺理成章的事,不会有技术断层。
unittest是python标准库自带的测试框架,最大的好处是不用额外装东西、不用额外学一套规则。你只要继承TestCase类,写一堆以test_开头的方法,它就能自动收集、执行、统计结果。很多人觉得unittest没有pytest灵活,这是事实。但对于UI自动化来说,unittest的“死板”反而是优势——用例的组织结构很规整,setUp和tearDown这种钩子方法特别适合处理每个用例都需要的打开浏览器、关闭浏览器动作。不要一上来就追新东西,先把标准库用透。
html就是测试报告。UI自动化的结果一定是要给人看的。你自己调试的时候可以看控制台输出,但项目上线、回归测试、给领导汇报,总不能让人家去看一堆cmd窗口滚动。一份结构清晰、有统计信息、有失败截图的HTML报告,能让整个工作的价值感完全不一样。
1.2 这套框架到底能解决什么问题
我见过很多测试团队“自动化”了半年,最后只留了一堆没人看的脚本。原因其实不是技术不行,而是从一开始就没想清楚框架要解决什么问题。我这套框架的目标非常明确,它解决的核心问题有三个:
第一,回归测试的效率问题。一个Web产品每次发版,核心流程至少要回归一遍。人工回归2个小时起步,自动化跑一遍20分钟搞定,而且晚上回家之前点一下执行,第二天早上看报告就行。第二,用例的管理和可维护性问题。通过Page Object模式封装页面元素和操作,页面变了只改一处,不用把所有用例全部重写。第三,结果的可视化问题。HTML报告里有总用例数、通过数、失败数、错误数、执行时长,每个失败用例都能看到堆栈信息、截图和日志,定位问题的时间能省一大半。
这里要强调一下,UI自动化不是用来替代手工测试的。它最适合做的是那些稳定模块的回归验证。你要是拿它去测那种需求天天变、界面天天改的功能,那确实会做得很痛苦。后面很多“自动化失败”的项目,其实都是栽在选错了测试对象上。
2. 环境准备与工程骨架搭建
2.1 Python环境与依赖安装
先说环境。这套框架最低要求是Python 3.8以上,推荐直接用3.10或3.11。如果你电脑上已经装了python,可以在命令行里输入python --version确认版本。没装的话,去python官网下载安装包,装的时候务必勾选“Add Python to PATH”这个选项,不然后面在命令行里敲python会提示找不到命令。
装完python之后,建议顺手把pip的源换成国内镜像,不然下载包的时候那速度会让你怀疑人生。Windows下在用户目录下创建一个pip.ini文件,Linux和Mac下是pip.conf,写入这样的内容:
[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple trusted-host = pypi.tuna.tsinghua.edu.cn这一步是我实测下来非常值得做的一步。我第一次搭环境的时候用默认源下载selenium,几MB的包等了七八分钟,换完镜像源之后基本是秒下。
然后安装核心依赖:
pip install selenium pip install webdriver-manager pip install html-testRunner这里稍微展开一下为什么用webdriver-manager。以前我们用selenium写脚本,需要手动去浏览器驱动官网下载对应版本的chromedriver,还得把驱动放到python的Scripts目录里或者指定路径。最坑的是浏览器一升级,驱动版本不匹配,脚本全部报错,又得重新下载。webdriver-manager这个库可以自动检测你本地的浏览器版本,自动下载对应的驱动,省掉了这些麻烦。
html-testRunner是用来生成HTML报告的库。注意一下,网上很多教程还在用HTMLTestRunner,那个是很多年前的版本了,还需要手动下载.py文件。html-testRunner是它的更新版,直接pip安装就能用,而且生成的报告样式更现代化,支持失败截图展示,强烈推荐用这个。
2.2 工程目录的设计思路
一套好用的框架,目录结构必须从第一天就设计清楚。不要等到写了几十个用例之后再重构。我常用的目录结构是这样的:
auto_ui_framework/ │ ├── common/ # 公共模块 │ ├── __init__.py │ ├── base_page.py # 页面基础类,封装元素定位、点击、输入等操作 │ ├── config.py # 全局配置,读入常量、base_url、超时时间等 │ ├── logger.py # 日志模块 │ └── driver.py # 浏览器驱动初始化 │ ├── pages/ # 页面对象层(Page Object) │ ├── __init__.py │ ├── login_page.py # 登录页相关元素和操作 │ └── home_page.py # 首页相关元素和操作 │ ├── test_cases/ # 测试用例层 │ ├── __init__.py │ ├── test_login.py │ ├── test_home.py │ └── test_suite.py # 批量执行入口 │ ├── reports/ # HTML报告输出目录 ├── logs/ # 运行日志输出目录 ├── screenshots/ # 失败截图输出目录 ├── requirements.txt # 项目依赖清单 └── run.py # 项目总入口,执行整个测试集这个结构的好处是分层清晰,每层干每层的事。pages目录放页面元素和操作,test_cases目录放业务断言,common放公共能力,谁跟谁都不纠缠。后面项目变大、用例变多,顶多增加几个子目录,也不用动结构。
有个容易被忽略的点,每个目录下都要放一个空文件__init__.py。python的包机制要求目录下必须有这个文件,才能被其他模块import进来。忘了这个文件,写代码的时候from pages.login_page import LoginPage就会报ModuleNotFoundError,很多人在这上面卡了半天。
2.3 公共配置与浏览器驱动管理
配置这块我习惯单独写一个config.py,里面集中放那些可能会变的参数。比如被测系统的base_url、浏览器的宽高、跑用例的默认等待时间、报告和日志的输出路径。这样后面接手项目的人,不需要翻遍所有代码去找一个藏在某个用例里的IP地址或者超时时间。
import os BASE_URL = "https://example.com" BROWSER_TYPE = "chrome" IMPLICITLY_WAIT = 10 # 隐式等待时长(秒) PAGE_LOAD_TIMEOUT = 30 # 页面加载超时(秒) DEFAULT_SCREENSHOT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "screenshots") REPORT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "reports") LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "logs")driver.py里的核心逻辑是这样的:
import os from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service as ChromeService from common.config import BROWSER_TYPE, IMPLICITLY_WAIT, PAGE_LOAD_TIMEOUT class WebDriverFactory: @staticmethod def get_driver(): if BROWSER_TYPE.lower() == "chrome": options = webdriver.ChromeOptions() options.add_argument("--start-maximized") # 防止selenium被识别导致登录异常,加一个隐藏参数(视项目情况) options.add_experimental_option("excludeSwitches", ["enable-automation"]) service = ChromeService(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) elif BROWSER_TYPE.lower() == "firefox": driver = webdriver.Firefox() else: raise ValueError(f"Unsupported browser type: {BROWSER_TYPE}") driver.implicitly_wait(IMPLICITLY_WAIT) driver.set_page_load_timeout(PAGE_LOAD_TIMEOUT) return driver--start-maximized参数让浏览器启动时直接最大化,避免因为窗口大小导致元素被遮挡而定位不到。excludeSwitches这个参数是用来把浏览器顶部的“Chrome正在受到自动软件的控制”这条提示去掉的,有些系统对它比较敏感,去掉之后更干净。这个是实践里发现的小技巧。
3. 核心封装:让用例写起来更顺手
3.1 页面对象模式(POM)的具体落地
Page Object模式,简称POM,是UI自动化框架里最核心的设计模式。它的大白话就是你为每一个页面写一个类,把页面上的元素定位写在这个类里面,把对页面的操作也写成这个类的方法。测试用例本身不直接接触元素定位,而是调用这些方法。
这样做的逻辑很简单:一个登录按钮,你可能在十个用例里都用到了。如果每个用例都自己去写driver.find_element(By.ID, "login_btn").click(),哪天按钮的ID变了,你就要改十个地方。但如果你把它封装在LoginPage类的click_login_button()方法里,只用改一个类文件就行。
我常用的base_page.py大概长这样:
import time import logging from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from common.config import DEFAULT_SCREENSHOT_DIR class BasePage: def __init__(self, driver): self.driver = driver self.logger = logging.getLogger(__name__) def find_element(self, locator, timeout=10): """locator格式为(By.ID, 'username') 或 (By.XPATH, '//input[@name="user"]')""" try: elem = WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) return elem except Exception as e: self._take_screenshot() self.logger.error(f"定位元素失败: {locator}, 错误: {e}") raise e def click(self, locator, timeout=10): elem = self.find_element(locator, timeout) elem.click() self.logger.info(f"点击元素: {locator}") def input_text(self, locator, text, timeout=10): elem = self.find_element(locator, timeout) elem.clear() elem.send_keys(text) self.logger.info(f"输入内容: {locator} -> {text}") def _take_screenshot(self): timestamp = time.strftime("%Y%m%d_%H%M%S") file_path = f"{DEFAULT_SCREENSHOT_DIR}/fail_{timestamp}.png" self.driver.save_screenshot(file_path) self.logger.info(f"截图保存到: {file_path}") return file_path所有页面的类都继承这个BasePage。login_page.py就变得非常清爽:
from selenium.webdriver.common.by import By from common.base_page import BasePage class LoginPage(BasePage): username_input = (By.ID, "username") password_input = (By.ID, "password") login_button = (By.CLASS_NAME, "btn-login") def input_username(self, username): self.input_text(self.username_input, username) def input_password(self, password): self.input_text(self.password_input, password) def click_login(self): self.click(self.login_button)这样写最直观的好处是,测试用例读起来就像自然语言一样。你不需要去猜这步在干什么,用例的可读性提升了一个档次。
3.2 unittest与selenium的结合方式
unittest和selenium结合的关键在于怎么巧妙地利用unittest的setUp和setUpClass这类生命周期方法。
先写一个base_test.py,让所有测试类都继承它:
import unittest from common.driver import WebDriverFactory class BaseTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.driver = WebDriverFactory.get_driver() @classmethod def tearDownClass(cls): cls.driver.quit() def setUp(self): # 每个用例开始前都回到首页,避免用例之间互相影响 self.driver.get(self.base_url) def tearDown(self): # 用例失败时自动截图,并挂在日志里 if hasattr(self, '_outcome'): result = self._outcome.result if result.errors or result.failures and id(self) in [id(f[0]) for f in (result.errors + result.failures)]: self.driver.save_screenshot(f"screenshots/{self._testMethodName}_fail.png") super().tearDown()这里有几个细节。setUpClass是类级别的初始化,整个测试类只启动一次浏览器。setUp是每个用例之前都会跑的,放回到首页让用例之间相互独立。tearDown里的那段判断逻辑,是检查当前用例是否有失败或错误,有的话自动截图。这个功能非常实用,后面排查失败原因的时候,有截图和没截图的效率完全是两个级别。
注意,如果你有几十个用例,setUpClass只启动一次浏览器,执行速度会快很多。但代价是用例之间的耦合度会变高,如果前一个用例把页面状态改得乱七八糟,后一个用例可能就受影响了。所以一般我建议用例内部或setUp里都要有回到稳定初始状态的逻辑。
3.3 三种等待机制到底怎么配合
UI自动化里最玄学的问题就是元素定位不稳定。明明手工点一点就出来了,自动化脚本一跑就找不到元素。百分之八十的原因出在等待上。
selenium里常用的等待方式有三种:强制等待、隐式等待、显式等待。
强制等待就是time.sleep(2),写起来简单,但它是最不推荐的方式。你永远不知道等2秒够不够,等多了又白白浪费时间。一个脚本里塞十几个sleep,跑一轮下来慢得你怀疑人生。
隐式等待是driver.implicitly_wait(10),它告诉浏览器,在找不到元素时最多等10秒钟。这个设置一次全局生效,代码里到处都不用再写等待。它的缺点是,如果某一个页面真的需要等15秒,10秒不够用;但大部分元素其实秒出,又白白多等了。所以一般建议设成一个保守的值,比如10秒甚至15秒。
显式等待是最灵活、最推荐的。就是前面base_page.py里用到的WebDriverWait,它可以针对某一个独立的元素设置独立的等待时间,还可以指定等待条件,比如元素可见、元素可点击、元素消失等等。
我个人的经验是,全局设一个15秒左右的隐式等待兜底,然后在关键业务操作上再用显式等待做精细化控制。比如登录按钮,等到它可点击了再点,比如弹窗,等到它出现了再操作。这套组合下来,脚本稳定性明显上升一个台阶。
4. HTML测试报告:数据可视化比想象中的重要
4.1 HTMLTestRunner的接入与参数配置
跑完用例之后,没有一份好报告,整个框架的可信度就大打折扣。这里我用的是html-testRunner这个库,它还兼容Python 3,风格也比较偏现代,支持把失败的截图直接内嵌到报告里。
看一下test_suite.py里怎么组织批量执行和生成报告:
import unittest import time import os from HtmlTestRunner import HTMLTestRunner from common.config import REPORT_DIR # 手动指定要执行的测试类 from test_cases.test_login import TestLogin from test_cases.test_home import TestHome if __name__ == "__main__": loader = unittest.TestLoader() suite = unittest.TestSuite() suite.addTests(loader.loadTestsFromTestCase(TestLogin)) suite.addTests(loader.loadTestsFromTestCase(TestHome)) timestamp = time.strftime("%Y%m%d_%H%M%S") report_file = os.path.join(REPORT_DIR, f"ui_test_report_{timestamp}.html") runner = HTMLTestRunner( output=REPORT_DIR, report_title="Web端UI自动化测试报告", report_name=f"ui_test_report_{timestamp}", combine_reports=True, add_timestamp=False ) runner.run(suite)combine_reports=True可以让多个测试类的报告汇总到一个HTML文件里,不然每个类都会单独生成一份报告,看起来很碎。report_title和report_name分别控制报告标题和文件名。加时间戳是为了保留历史报告,方便后面对比某次版本变更前后用例通过率的变化。
跑完之后,在reports目录下会生成一个HTML文件,直接双击就能在浏览器里打开。报告里会有总用例数、通过数、失败数、错误数、跳过数、运行时长这些统计信息,每一行用例的通过状态、错误信息也都列得清清楚楚。
4.2 报告里的失败信息怎么写才够用
刚搭框架的时候,我犯过一个很典型的错误:断言写得太粗,失败之后根本看不出来是哪个环节出了问题。比如登录用例,我只写了self.assertEqual(driver.current_url, "https://example.com/home"),如果登录失败了,报告里只告诉我URL不对,但到底是页面没加载出来、按钮没点进去、还是用户名密码错误了,完全不知道。
后面我给自己定了一个规矩:每个关键步骤之后都要有明确的状态校验,断言必须带上下文信息。
class TestLogin(BaseTest): base_url = "https://example.com/login" def test_login_success(self): self.driver.get(self.base_url) login_page = LoginPage(self.driver) login_page.input_username("admin") login_page.input_password("123456") login_page.click_login() # 等待登录成功后的某个特征元素出现 home_page = HomePage(self.driver) self.assertTrue( home_page.is_user_info_displayed(), "登录后主页的用户信息区域未显示,登录可能失败" ) def test_login_wrong_password(self): self.driver.get(self.base_url) login_page = LoginPage(self.driver) login_page.input_username("admin") login_page.input_password("wrong") login_page.click_login() error_msg = login_page.get_error_message() self.assertIn("用户名或密码错误", error_msg, f"错误提示内容不符,实际提示为: {error_msg}")这样做之后,报告里每一条失败信息都是可追溯的。哪怕是团队里其他人或者刚入职的测试同学来看报告,也能很快明白用例在验证什么、失败可能是什么导致的。别小看这一点,我见过一个报告里全是“Expected True, got False”的项目,那可真叫一个崩溃。
4.3 失败重跑和自定义报告扩展
验收测试是一个脏活,UI自动化跑在改动频繁的浏览器环境里,总会遇到偶发性的失败。点一下没点上、动效没走完、网络抖了一下,这类问题可能跟代码逻辑没关系,纯属环境抖动。
我采用的策略是“失败自动重跑一次”。实现起来也不复杂,自定义一个unittest的TestResult类,或者用现成的库rerun也可以。但更简单的方式是在用例执行层做一次循环:
class TestCaseWithRetry(unittest.TestCase): _retry_count = 2 # 重试次数 def run(self, result=None): current_frame = None for attempt in range(self._retry_count + 1): if current_frame: self._outcome = None current_frame.clear() result = super().run(result) if result.wasSuccessful(): break return result注意这里有个复杂的细节:unittest的TestResult对象在重跑时状态是累积的,所以每轮重跑前要清理掉上次结果。这实现起来比较绕,更稳妥的方案是用pytest的flake-rerun插件来做,或者用一个小型装饰器在套件层做外层循环。
重跑逻辑要克制,不能无限重试。一次失败后重试一次,能覆盖大部分偶发问题;重试两次以上,效果反而不明显,还容易把真正的bug掩盖掉。一路重试到绿,那不是自动化测试,那是自欺欺人。
5. 测试执行与持续集成
5.1 批量执行、挑选用例与并发问题
最初跑用例是一个套件把所有用例全跑一遍。但当用例数量涨到一两百个之后,问题就来了:跑一轮要一小时,一个小改动引发的回归就要等这么久,太慢了。
解决思路有两个方向:一个方向是按影响范围挑选用例,比如改动了登录模块,就只跑登录相关的用例;另一个方向是并发执行,多个浏览器同时跑不同的用例。
unittest本身不支持并发,但可以借助unittest的TestSuite配合多线程自己写一个并发启动器。大概思路就是把测试类按类分配到不同的线程里,每个线程启动一个浏览器进程。这里有一个非常关键的坑:如果你用了setUpClass去共享同一个浏览器驱动,那么不同线程之间的driver每次都要重新创建,不要试图用一个全局driver变量绕过去,selenium的WebDriver不是线程安全的,强行共享会引发各种诡异异常。
还有一套更稳的方案,就是把测试执行命令交给pytest。pytest虽然和unittest的写法不同,但它可以直接运行unittest风格的用例,并且自带pytest-xdist插件支持分布式并发执行。命令行里加一个-n 4就能用4个并发进程跑用例。如果你的用例彼此独立,这一步优化的效果会非常显著。
5.2 接入Jenkins实现定时回归
写好的自动化框架如果不接入持续集成系统,那就只能算是本地工具,谈不上真正的测试基建。我这里以Jenkins为例说一下关键接入点。
Jenkins里新建一个自由风格的项目,源码管理选择你的git仓库,构建环境里选上“Add timestamps to the Console Output”,这样看日志的时候能知道每步的耗时。最核心的是构建命令:
cd /your/project/path python -m pip install -r requirements.txt python test_cases/test_suite.py构建后操作里,选择“Publish HTML reports”,填上报告目录和报告文件。这样每次构建结束后,首页就会有一个可以点击的“Latest Test Report”链接,点开就是一份完整的HTML报告。
为了让报告更容易浏览,我还会在构建后加一个邮件通知。用Email Extension Plugin,把HTML报告作为附件发出去,或者只发一个包含测试结果摘要的邮件。这样每天早上一封邮件,团队所有人都能看到昨晚回归的情况。
接入Jenkins后的运行时间建议放在晚上。白天电脑办公、网络波动、浏览器弹窗都可能干扰测试;晚上环境干净很多,执行结果也更可信。第二天早上上班第一件事就是打开Jenkins看报告,有问题第一时间反馈给开发,这种方式已经被验证非常高效。
5.3 执行策略:定时、轮询还是人工触发
这里想多说一句关于“到底该怎么触发UI自动化测试”的建议。很多人搭完框架就兴奋地设了个每天晚上10点定时跑的任务,结果每天早上都收到一堆失败报告,一个月后大家的习惯就变成了“看到这个邮件都不打开了”。这会直接毁掉自动化测试在团队里的信任度。
我个人的经验是,循序渐进。一开始只做“人工触发”,也就是只在发版前或需求提测的时候手动点一下Jenkins执行,先跑一周,把那些天天失败的用例修正一遍;稳定之后再改成“定时触发”,比如每天夜里回归一次;最后再考虑在代码合并流水线里加一个“冒烟测试”阶段,只在push到主分支时跑核心用例,跑挂了就直接拦住合并请求。
要让团队相信自动化结果,先要做到自动化的报告可靠。一上来就追求全自动化和高频率,往往是压垮项目的最后一根稻草。
6. 踩坑记录与问题排查速查表
6.1 元素定位不稳定的深层原因
UI自动化最大的敌人就是“昨天还能跑,今天突然挂了”。这类问题的排查,我基本遵循一个固定的思路:先看报告里的错误信息,是NoSuchElementException、ElementClickInterceptedException还是TimeoutException。
NoSuchElementException是找不到元素,最常见的原因是定位表达式变了或者元素还没加载出来。先换成显式等待看能不能解决;解决不了就去页面看看是路径变了还是id变了。
ElementClickInterceptedException是元素被什么东西挡住了,跑脚本的时候经常遇到弹窗、浮层、Cookie提示条。解决办法是先关掉这些遮挡元素,或者用WebDriverWait等待遮挡元素消失。
TimeoutException多发生在显式等待超时,比如等待一个弹窗出现但一直没出现,那就要考虑是不是某个前置操作没成功,业务逻辑本身报错了。这时候报告里有没有截图就派上大用场了。
除了这些问题,还有一类很隐蔽的坑:同一个页面元素在开发环境、测试环境、正式环境里的定位完全不同。这个基本只能靠配置文件里分环境设定不同的定位符来解,别无他法。
6.2 WebDriver版本不匹配与浏览器更新陷阱
用webdriver-manager解决了大部分驱动问题,但偶尔还是会遇到“SessionNotCreatedException: This version of ChromeDriver only supports Chrome version xxx”。这个问题通常发生在浏览器自动升级之后。
ml 解决方案很简单,把webdriver-manager升级到最新版,然后清理掉缓存的本驱动:
pip install --upgrade webdriver-manager webdriver-manager clean另外尽量不要在自动化服务器上开启浏览器的自动更新。Windows系统可以通过组策略关闭Chrome自动更新,避免夜里升级完了第二天起来自动化全家挂掉。这些看起来细枝末节的东西,执行起来都是影响框架稳定性的关键。总之一句话,自动化环境里的浏览器版本,要跟依赖的webdriver版本严格保持一致。
6.3 并发执行与数据隔离
做并发执行的时候,测试数据隔离是另一个大坑。假设你有两个用例同时登录同一个账号,一个用例改了密码,另一个用例立刻受影响;再比如两个用例同时创建一个相同名称的订单,第二个用例就创建失败了。不解决数据问题,并发执行就是灾难现场。
我的习惯是,每个测试用例都用独立的测试数据。要么在用例初始化时通过接口造数据,要么把测试账号池化,每个并行进程分配一个独立的账号。实在躲不开共用数据的场景,就只能把这些用例放到串行队列里跑,牺牲一点速度换来稳定。
6.4 常见问题排查速查表
这里整理一下我在实践中最常遇到的十来个问题,做成一张速查表。
| 现象 | 可能原因 | 排查与解决方法 |
|---|---|---|
| 启动脚本就不停报session not created | 浏览器和webdriver版本不匹配 | 升级webdriver-manager,并清理缓存 |
| 一直找不到某个元素 | 页面加载太慢或元素在iframe里 | 先用显式等待,检查是否要switch_to_iframe |
| 按钮点击没反应 | 页面上的元素被浮层遮挡 | 查看报告截图,先关闭遮挡元素再点击 |
| 点击后进入不了下一页面 | 页面跳转有时间窗口 | 用WebDriverWait等待页面特征元素出现 |
| 用例偶发失败 | 网络波动、动效未结束、环境脏数据 | 考虑失败重跑机制,先重试一次 |
| 报告里没有截图 | 失败时driver状态已经异常 | 在tearDown中捕获异常后再截图 |
| 并发跑时数据相互干扰 | 用例没有独立测试数据 | 引入测试账号池或隔离测试数据集 |
| 本地能跑,Jenkins上跑不了 | Jenkins服务账号没有图形界面或权限受限 | 配置xvfb,或改用headless模式 |
| 登录状态总是失效 | 浏览器上下文没共享,登录token没保存 | 用同一浏览器实例串行执行,或者统一走接口处理登录 |
| 执行时间越来越长 | 用例越来越多,HTTP慢请求没优化 | 引入并发执行,或精简用例集 |
7. 写在最后的实操心得
这套python+unittest+html的UI自动化测试框架,从环境搭建到跑出第一份报告,正常一两天就能搞定。整体项目做下来,我个人的体会是:框架只是手段,质量和效率才是目的。任何花里胡哨的技术方案,如果不能帮团队在日常回归中节省时间、提前发现问题,那它就是个“面子工程”。
实际使用中还有几点想额外提一下。第一,测试框架的代码本身也要写注释、做评审,它和业务代码一样需要维护;第二,不要过度封装,封装过深会让脚本变得特别“绕”,排查问题的时候要跳好几层才找到真正的逻辑;第三,UI自动化需要持续调优,运行一段时间后要定期去看报告里的失败用例,把那些因为环境原因反复失败的用例修掉或者标成跳过,保持报告的真实置信度。
最后分享一个小技巧,我在框架里加了一个run.py的入口,可以直接用命令行参数指定跑哪个模块、是否开启重试、是否生成报告。这样测试同学不需要了解代码,只要记住几个命令就能执行用例。让工具尽可能简单,让使用者尽可能省心,这才是自动化测试框架能真正落地和长期跑下去的关键。