1. 先把“跑用例”和“看结果”拆成两件事
很多人第一次接触 selenium 自动化测试,注意力全在“怎么把元素点出来”上,等脚本能跑通了,就觉得自己会了。真正进到项目里才发现,几十个测试用例堆在一起,跑一次要等十分钟,跑完还不知道哪个挂了、为什么挂、挂了之后看哪张截图。这就是典型的“用例能跑,但跑得不明白”。这篇我接着之前的 selenium 系列往下聊,主题是测试用例运行和报告,面向的是已经会写一点点 python、能看懂 selenium 基本定位,但在用例组织和结果呈现上还是新手的同学。核心要解决三个问题:怎么把散落的用例组织成一个能一键执行的整体,怎么用运行器把执行结果稳定地吐出来,怎么把这些结果变成一份别人(或者明天的你自己)能看懂的测试报告。
标题里“非 python 新手”这个限定,我理解成两层意思。第一层,你不需要是 python 专家,能写函数、能看懂类和装饰器就够了;第二层,这一篇不讲 python 语法入门,默认你已经装好了 selenium 和数据驱动那套东西。所以我会把重心放在运行机制和报告产出上,把背后“为什么这么做”讲透。我自己踩过的坑里,一半以上都不在业务逻辑,而在于用例怎么被发现的、报告为什么是空的、截图为什么存到了奇怪的地方。这些坑,才是这一篇真正想给你的东西。
2. 运行器的选型:unittest 和 pytest 到底怎么挑
2.1 两种运行器的本质差别
python 生态里能跑测试用例的运行器不止一个,但日常用得最多的就是标准库自带的 unittest 和第三方库 pytest。unittest 是 python 标准库的一部分,装好 python 就有,它的设计参考了 Java 的 JUnit,讲究类继承、方法命名、断言语义统一。你写一个类继承unittest.TestCase,方法名以test_开头,运行器就会自动把它当作一条用例。这个约定的好处是零依赖、结构清晰,坏处是写起来啰嗦,参数化、夹具、跳过条件的写法都不够顺手。
pytest 则是另一套思路。它不要求你继承任何类,普通的函数只要名字以test_开头就能被识别成用例,断言直接用 python 原生的assert。它自带的夹具(fixture)机制比 unittest 的 setUp/tearDown 灵活得多,参数化、失败重跑、并行执行都有成熟的插件生态。代价是你得额外装一个包,团队里如果要求“绝对不能引入第三方依赖”,那就只能退回 unittest。
我给新手一个很朴素的判断标准:如果这个自动化项目是个一次性验证脚本,或者团队严格限制依赖,用 unittest;如果这是要长期维护、用例会从几十条涨到几百条的工程化项目,直接上 pytest。别纠结,这个决定拖久了反而是浪费。
2.2 一个入口脚本的价值
不管你选哪个运行器,我都强烈建议你把“执行入口”和“用例代码”分开。所谓执行入口,就是单独一个run_tests.py或者直接一条命令行,它不关心具体用例写了什么,只负责找到用例、依次执行、生成报告。用例文件里只放业务操作和断言,不放任何运行相关的配置。这样做的理由很实在:用例代码是要经常改的,执行入口基本不动;把两者混在一起,换个报告格式就得翻遍所有用例文件,那才是真的给自己找事。
我见过不少人把unittest.main()直接写在每个用例文件末尾,然后靠手动一个个跑文件来执行。这种方式在只有两三条用例时没问题,一旦用例多了,你会发现自己大部分时间花在“记住今天该跑哪几个文件”上,而不是在验证功能。把入口收拢成一个点,是自动化测试从玩具走向工具的第一步。
2.3 目录结构先定好,后期少折腾
在写运行逻辑之前,先把目录结构定下来,能省掉后面大半的路径问题。我常用的结构是这样:项目根目录下放testcases目录存用例,pages目录存页面对象封装,common目录存公共方法,reports目录存报告输出,screenshots目录存失败截图。运行入口放在根目录,通过相对路径引用各个子目录。
这个结构不是唯一解,但它的好处是每个目录职责单一。尤其注意报告和截图这两个输出目录,一定要提前建好,并且在脚本里用代码判断目录是否存在、不存在就创建。很多新手第一次跑报告,脚本报错说找不到输出路径,其实就是因为reports目录压根不存在。这种问题排查起来很快,但发生频率高得离谱,不如一开始就用os.makedirs(..., exist_ok=True)兜底。
3. 测试用例怎么写才能被运行器正确发现
3.1 命名约定不是形式主义
运行器找用例靠的是命名约定,这是最容易踩坑的地方。unittest 的discover方法默认匹配的文件模式是test*.py,注意是test开头,中间没有下划线也照样匹配。很多人习惯写test_login.py,这没问题,但也有人写成login_test.py,结果运行器根本没找到这个文件,还以为是运行命令写错了。pytest 默认匹配test_*.py和*_test.py两种,宽容一些,但类名和方法名还是有要求。
我建议团队内部统一成test_开头,因为这是最主流的写法,无论换哪个运行器都不会出问题。类名用Test开头,方法名用test_开头,这两条守住了,运行器的发现机制基本不会出错。不要在这个地方耍个性,比如用中文命名或者特殊前缀,短期看没什么,等接手的人来了会骂人的。
3.2 前置和后置操作的放置位置
每条用例执行前后往往要做一些准备工作,比如打开浏览器、登录、清理数据。unittest 提供了setUp和tearDown,分别在每条用例前后执行;还有setUpClass和tearDownClass,在整个测试类前后各执行一次。这个区分非常重要:如果你的用例是“登录后验证 A”“登录后验证 B”,那么登录这个动作应该放在setUpClass里只做一次,而不是每条用例都重登一遍。省下来的时间,在几十条用例规模下会非常可观。
pytest 里对应的概念是 fixture,通过scope参数控制作用范围,function级别相当于 setUp,class级别相当于 setUpClass,session级别则贯穿整个测试会话。对于 selenium 这种启动浏览器开销较大的场景,我通常把浏览器初始化放在 class 或 session 级别,把页面登录放在 class 级别,用例本身只做断言。这样一次跑几十条用例,浏览器只开几次,速度能快好几倍。
注意:
setUpClass里初始化的对象,在setUp里要用类变量而不是实例变量来引用,否则每个用例都会拿到不同的实例,共享状态就失效了。
3.3 用例之间不要互相依赖
这一点我要单独拎出来讲,因为它是新手最容易犯、后果又最严重的错误。假设你有三条用例:测试登录、测试下单、测试支付。如果你写成“支付用例依赖下单用例先执行”,那么一旦下单用例失败,支付用例也会连带失败,报告上会显示一片红,但真正的根因只有一个。更糟的是,如果运行器调整了执行顺序(pytest 在某些配置下并不保证严格按字母序),整个结果就乱了。
正确做法是每条用例都应该是自洽的,能从干净状态开始,跑完能自己收拾干净。需要登录就在自己的前置里登录,需要商品数据就自己造。这确实会让单条用例的执行时间变长,但换来的是结果可信。报告的意义在于告诉你“哪个功能坏了”,如果用例之间有依赖,报告就变成了“哪个组合坏了”,诊断价值大打折扣。如果你实在有共享的前置数据,用 fixture 或 setUpClass 来提供,而不是靠一条用例给另一条用例铺路。
4. 命令行运行测试用例的完整实操
4.1 unittest 方式的运行命令
先说不依赖任何第三方的跑法。假设你的用例都在testcases目录下,在项目根目录执行下面这条命令:
python -m unittest discover -s testcases -p "test_*.py" -v参数逐个拆解一下。-s指定从哪个目录开始搜索,-p指定文件名匹配模式,-v是 verbose,会输出每条用例的名字和执行结果,而不是只给一个点号。新手阶段我强烈建议始终加-v,因为你要靠这个输出来判断用例到底有没有被发现、执行顺序对不对。等用例稳定了,再考虑去掉做静默执行。
如果你只想跑某一个测试类,可以这样:
python -m unittest testcases.test_login.TestLogin -v这里用的是模块路径加点类名的方式,模块之间用点分隔,注意不要写成文件路径带斜杠,那是另一个概念。我见过有人写python -m unittest testcases/test_login.py,能跑起来但不规范,遇到包结构复杂一点就会出问题。
4.2 pytest 方式的运行命令
pytest 的命令行更简洁,直接在根目录:
pytest testcases -v如果只想跑名字里带 login 的用例,用-k做筛选:
pytest testcases -v -k "login"-k支持简单的表达式,比如-k "login and not register",可以排除掉不想跑的用例。这个功能在做冒烟测试时特别好用,我只想快速验证核心链路,就用-k圈一个子集出来,几十秒跑完。-s参数用来关闭输出捕获,让你能看到代码里的 print 内容,调试时很有用;--maxfail=3表示失败三条就停止,避免一个批量问题导致几百条用例全部白跑。
pytest 还有一个我特别喜欢的参数--lf,全称 last-failed,只会重跑上一次失败的用例。这个在修复 bug 的循环里效率极高:改完代码,直接pytest --lf验证,不用等全量用例跑完。
4.3 失败重跑机制的配置
自动化测试有一类失败非常恶心:网络抖动、页面加载慢、动画没结束。这类失败不是你的代码有问题,而是环境不稳定。应对办法是给用例加失败重跑。pytest 用pytest-rerunfailures插件,装好后加参数:
pytest testcases -v --reruns 2 --reruns-delay 1意思是失败后重跑两次,每次间隔一秒。如果两次都失败,才判定为真正失败。这样做能有效过滤掉偶发的环境问题,让报告里的红色都是真问题。
不过重跑有个陷阱要提醒你:它会让执行时间变长。如果你有 200 条用例,其中 20 条偶发失败,每条重跑两次,那总时间会增加不少。所以重跑次数不要设太高,两次是经验值,三次以上基本没必要,因为连续三次都失败通常说明问题不是偶发的。另外重跑的用例在报告里要能区分出来,否则你会误以为是两条不同的失败用例。
4.4 执行环境的隔离
跑用例之前,确认你的执行环境是干净的。这里说的干净有两层:一是浏览器状态干净,没有残留的 cookie 和缓存;二是测试数据干净,上一次跑留下的数据不会干扰这一次。浏览器这边,我通常在初始化时用无痕模式,或者在 tearDown 里调driver.delete_all_cookies()。数据这边,如果测试会写数据,要在前置里准备好、后置里清掉,或者每次都用时间戳生成唯一的数据标识。
对于团队协作的项目,建议把待测环境的地址、账号密码这些配置抽到一个单独的配置文件里,不要硬编码在用例中。这样换环境的时候只改一个地方,而且敏感信息也不会跟着代码进版本库。配置文件用config.ini或者.env都行,python 都有对应的读取方式,选一个团队都熟悉的就好。
5. 测试报告的生成与结果呈现
5.1 unittest 搭配 HTMLTestRunner
unittest 自带的文本输出只能看个大概,要做报告得借助第三方库。HTMLTestRunner 是个经典选择,它把执行结果渲染成一个 HTML 页面。不过这里有一个大坑:网上流传的很多 HTMLTestRunner.py 是 python2 时代的产物,直接拿来在 python3 下用会报各种编码和类型错误。你需要找适配 python3 的版本,或者用 pip 安装html-testRunner这个包。
用适配版本写出来的入口大概长这样:
import unittest import os from HTMLTestRunner import HTMLTestRunner if __name__ == '__main__': report_dir = 'reports' os.makedirs(report_dir, exist_ok=True) suite = unittest.defaultTestLoader.discover('testcases', pattern='test_*.py') report_path = os.path.join(report_dir, 'result.html') with open(report_path, 'w', encoding='utf-8') as f: runner = HTMLTestRunner( stream=f, verbosity=2, title='自动化测试报告', description='每次执行后自动覆盖' ) runner.run(suite)这段代码里有几个细节值得注意。stream参数接收的是文件对象,不同版本的 HTMLTestRunner 对文件打开模式要求不一样,有的是二进制wb,有的是文本w,用之前先看一眼包的说明,或者直接试一下。title和description是报告页面上显示的标题和描述,别小看这两项,把项目名和执行环境写进去,翻报告的人能省下很多猜测。报告文件名我用固定的result.html,每次都覆盖,配合日期目录来区分不同批次。
5.2 pytest 搭配 pytest-html 和 Allure
pytest 的报告生态更丰富,装pytest-html后加一个参数就能出报告:
pytest testcases -v --html=reports/result.html --self-contained-html--self-contained-html这个参数一定要加,它会把 CSS 和 JS 内联进 HTML,生成一个单文件。否则报告会依赖一堆外部资源,你发给别人看的时候样式全丢,对方还以为你报告坏了。pytest-html 的优点是轻量、零配置,缺点是样式比较朴素,细粒度信息展示有限。
如果你需要更专业的报告,比如按功能模块分组、带失败截图的步骤详情,那就上 Allure。它的流程是分两步的:
pytest testcases --alluredir=./allure-results allure serve ./allure-results第一步执行用例并把原始数据存到目录,第二步启动一个本地服务把数据渲染成网页。Allure 的报告信息密度高,失败用例能直接展开看到截图和日志,非常适合给团队看。代价是环境配置多一步,需要装 Allure 的命令行工具,团队共享报告时也要统一版本。
| 报告方案 | 依赖成本 | 报告颜值 | 适合场景 |
|---|---|---|---|
| unittest 文本输出 | 无 | 低 | 本地快速调试 |
| HTMLTestRunner | 一个适配包 | 中 | unittest 项目、简单分享 |
| pytest-html | 一个插件 | 中 | pytest 项目、日常输出 |
| Allure | 插件加命令行工具 | 高 | 团队协作、长期维护 |
5.3 失败截图与日志的联动
一份只有红色文字的失败报告,价值有限;一份带失败现场截图的报告,价值翻倍。截图的基本写法是driver.get_screenshot_as_file(path)或者driver.save_screenshot(path),前者返回布尔值方便判断成功与否。问题在于,怎么让截图和具体的失败用例对应起来。
我的做法是在用例的失败处理逻辑里截图,文件名带上用例名和时间戳,比如test_login_20250101_103000.png。如果是 pytest,可以写一个 hook 函数,在用例失败时自动抓取当前 driver 的截图。这样报告里每条失败用例都能挂上对应的截图,排查时不用去猜当时页面长什么样。
日志同理,把关键操作和信息用 logging 模块记下来,输出到文件。截图负责“看到”,日志负责“知道发生了什么”。两个配合起来,一个页面加载失败的问题,你既能看到白屏的截图,又能从日志里看到是卡在了哪个元素等待上。
提示:截图存的是相对路径时,要注意当前工作目录。运行器从哪个目录启动,相对路径就相对于哪个目录。用绝对路径最稳妥,或者统一在配置里定义一个输出根目录。
6. 失败用例排查与稳定性治理
6.1 用例跑不起来或者一条都没执行
这是最高频的问题,表现是命令执行了,但输出里显示Ran 0 tests或者收集到 0 个用例。原因通常就那几个:文件命名不匹配,检查是不是test_开头;目录路径传错,运行器在错误的目录里搜索;测试类没有继承unittest.TestCase(unittest 场景);测试方法名没有以test_开头。排查顺序建议从外到内,先确认目录对不对,再确认文件名,最后看类名和方法名。
还有一个隐藏原因是__init__.py。如果你的用例放在一个包目录里,某些运行方式需要目录下有__init__.py才能被正确导入。这个在不同 python 版本和运行方式下行为有差异,遇到了直接加上试试,成本很低。
6.2 报告生成成功但是内容为空
报告文件确实生成了,打开一看全是零或者显示错误。这种情况第一要检查报告生成代码是不是在runner.run(suite)之前就把文件关闭了,那当然写不进内容。第二要检查stream用的文件模式对不对,模式用错会导致写入失败但不报错。第三,有些 HTMLTestRunner 版本对报告内容有编码要求,如果用例名里有中文,可能在渲染阶段出问题。统一用英文命名用例,报告本身用中文标题和描述,这样最稳。
6.3 报告中文乱码
HTMLTestRunner 生成的中文乱码是老问题了。根因是页面声明了 UTF-8 但写入时没有按 UTF-8 编码,或者反过来。解决办法是在打开文件时显式指定encoding='utf-8',同时确认 HTMLTestRunner 内部生成 head 时写的 charset 也是 utf-8。如果包本身写死了别的编码,那就得改包源码或者换一个维护更活跃的版本。pytest-html 在这块表现好很多,基本不会有乱码问题,这也是我推荐新手直接用 pytest 的原因之一。
6.4 偶发失败怎么定位
偶发失败的定位比稳定失败难得多,因为复现不了。我的经验是按这个顺序排查:先看是不是等待不足,很多偶发失败都是页面元素还没出来就去找了,加显式等待往往能解决;再看是不是测试数据冲突,多条用例用了同一个账号或同一批数据,并行或串行时互相干扰;最后看是不是环境本身不稳,比如数据库响应慢、网络抖动。前两类靠改代码解决,第三类靠重跑机制过滤。
我整理了一份常见问题的速查表,遇到时可以直接对号入座:
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 一条用例都没执行 | 命名不符、路径错误、类未继承 | 检查命名与目录结构 |
| 报告为空 | 文件提前关闭、模式错误 | 检查报告生成代码 |
| 中文乱码 | 编码未统一 | 显式指定 utf-8 |
| 截图找不到 | 路径相对目录不对 | 改用绝对路径 |
| 用例偶发失败 | 等待不足、数据冲突 | 加显式等待、隔离数据 |
| 执行时间过长 | 浏览器频繁启停 | 提升 fixture 作用域 |
6.5 让报告真正被用起来
最后聊一个比技术更重要的事。报告生成出来,如果没人看,那它就只是一堆文件。我在项目里会让报告固定输出到一个共享目录,文件名带上日期和批次,再配合一个简单的汇总说明,比如今天跑了多少条、通过多少、失败哪几个模块。这样产品、开发、测试都能在同一个页面看到进度,而不是每次跑来问“测了吗,结果怎么样”。
对于长期维护的项目,我还会记录每次执行的通过率趋势。如果某个模块的通过率持续下降,即使这次还没红,也说明代码在变脆,值得提前介入修一修等待逻辑或者定位方式。这种“用报告驱动优化”的习惯,比单纯会用某个报告工具价值大得多。
踩过几次坑之后,我越来越觉得,自动化测试里真正拉开差距的不是定位写得多花哨,而是运行和报告这一层有没有做扎实。用例组织清楚、执行命令固定、报告有人看,这套流程跑顺了,你后面做数据驱动、做持续集成,都会少很多返工。