资讯动态

Playwright+Allure集成:自动化测试截图与视频报告实战指南

发布时间:2026/10/21 9:49:59 来源:尧图企业网站定制
做自动化测试这几年我一直有个执念报告里光给一堆红色断言错误根本没法让同事一眼看懂到底哪里出了问题。直到我把Playwright的录屏和截图真正接进Allure报告情况才彻底改观——现在每轮回归后开发点开报告就能看到崩溃前的最后几秒画面再也不用反复跑来问“诶这个用例怎么挂了”。这篇教程就围绕这件事展开怎么用Pytest组织Playwright用例怎么在最合适的时机抓截图、录视频最后怎么把这些附件干净整齐地挂到Allure报告里。整个过程我拆得非常细几乎每一步都配有代码和踩坑说明照着做就能直接落地。1. 为什么要在Allure报告里集成截图和视频1.1 默认报告不够直观你只看到结果没人看到现场Allure本身的交互和步骤记录已经做得挺好但对UI自动化来说文字描述永远代替不了“现场画面”。一个用例断言失败Allure里只写了expected 登录成功 but got 密码错误开发拿到这条信息后第一反应往往是截图呢当时的页面长什么样这种情况在真实项目里每天都会发生。我参与过的某电商项目回归用例三百多条失败的十条里至少七条是环境问题、弹窗遮挡、按钮被其他元素盖住这类“看截图一眼就懂、看日志半天理不清”的毛病。没有图片和视频排查时间轻松翻倍。而这个需求其实只花半天就能满足——把Playwright的截图能力和Allure的attachment机制对接起来。1.2 截图和视频各解决什么问题截图是静态快照适合回答“失败那一刻页面是什么状态”。视频是动态过程适合回答“失败之前发生了什么”。两者互补不能互相替代。实际执行里我常用的组合是每个测试步骤存一张关键截图失败时可以快速定位是哪一步开始异常用例结束或失败时存一张整页截图方便看全貌整个测试过程用Playwright的上下文录屏提供从浏览器打开到用例结束的完整视频。截图足够解决八九成的排查问题视频则用来应对那些“偶现、难以复现、看着看着就崩了”的疑难杂症。有了这套组合我这边接手他人用例时几乎不需要再手动复现一次光看报告就能判断问题归属。1.3 为什么不直接用Playwright原生HTML报告Playwright自带的HTML报告也能看到trace和截图但它有几个短板一是和现有测试体系融合度低如果公司已经用Allure做历史趋势和统计再多一套报告就割裂了二是原生报告默认不保留视频回放trace viewer虽然强大但对非测试人员来说反而门槛高三是Allure的生态成熟可以和CI、钉钉/飞书通知、统一平台的对接做到一处管理。所以我的结论很直接Playwright负责“采集”Allure负责“展示”。Playwright只干它最擅长的事——控制浏览器、截图、录屏然后把产物交给Allure渲染。这套分工是经过多个项目验证的比强行在Playwright报告里解决问题要舒服得多。2. 环境准备与依赖安装2.1 依赖清单先说版本。这套方案依赖的库不算多但版本之间确实有过不兼容的情况我把当前稳定可用的组合列一下库版本建议作用Python3.9运行环境pytest7.4测试框架Playwright1.40浏览器自动化pytest-playwright0.4Pytest与Playwright的桥接allure-pytest2.13生成Allure所需数据allure2.24命令行工具用于生成报告pytest-playwright这个库挺关键它提供了一堆现成的fixture比如page、context、browser还能直接读Playwright的配置。不过它默认的视频录制是通过启动参数控制的这和我们想要的“按用例动态控制”不太匹配所以后面我大部分时候是自己定义fixture只用它提供的page作为基础。2.2 安装命令我一般用venv隔离环境避免把全局Python搞乱。安装命令如下python -m venv venv source venv/bin/activate # Windows用venv\\Scripts\\activate pip install -U pytest playwright pytest-playwright allure-pytest playwright install chromiumplaywright install chromium这步很多人会漏掉浏览器没装好后面跑什么都不动。如果你还要测WebKit或Firefox就继续装对应的浏览器不过多数项目Chromium就够用了。2.3 项目目录结构一个干净的结构会让维护省很多心。我通常这样组织project/ ├── tests/ │ ├── conftest.py │ ├── test_login.py │ └── test_order.py ├── assets/ │ ├── screenshots/ │ └── videos/ ├── reports/ │ └── allure-results/ ├── pytest.ini └── requirements.txtassets目录用来临时存放截图和视频注意它应该被.gitignore忽略因为这些是运行时产物没必要进版本库。reports目录同理。后面所有代码都围绕这个结构展开。清理思路也要提前想每次跑完一轮用例assets里可能堆几十个视频文件。我会在CI的构建阶段把整个目录清掉再执行测试避免上一轮的旧附件混进本轮报告。3. 截图的三种姿势与进阶用法3.1 失败自动截图的fixture实现先写一个最实用的用例失败时自动截图并把图片挂到Allure里。在conftest.py中实现利用pytest的pytest_runtest_makereport钩子import allure import pytest from pathlib import Path pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: # 从fixture里拿page实例 page item.funcargs.get(page) if page is not None: screenshot_path Path(assets/screenshots) / f{item.name}.png page.screenshot(pathstr(screenshot_path), full_pageTrue) allure.attach.file( str(screenshot_path), namef{item.name}失败截图, attachment_typeallure.attachment_type.PNG, )这段代码的逻辑是每个测试用例结束后pytest都会走到这个钩子。如果when call阶段报告为失败就尝试从测试函数的参数里取出page对象截图保存再通过allure.attach.file把图片写进结果里。你可能会问为什么不用allure.attach而是用allure.attach.file因为attach直接把图片二进制塞进内存对超大截图可能会让内存膨胀。attach.file则是传文件路径等生成报告时再读文件对大图友好得多。3.2 步骤级截图的装饰器封装失败自动截图能解决“最后一眼”的问题但有时候你更想知道“哪一步开始不对劲”。这时候就需要步骤级截图。我封装了一个装饰器玩法是在每个操作后自动截图。核心思路是拦截被装饰的函数执行完真实操作后再补一刀import allure from functools import wraps def step_with_screenshot(func): wraps(func) def wrapper(page, *args, **kwargs): result func(page, *args, **kwargs) screenshot page.screenshot(full_pageFalse) allure.attach( screenshot, namef步骤: {func.__name__}, attachment_typeallure.attachment_type.PNG, ) return result return wrapper # 用法示例 class LoginPage: step_with_screenshot def fill_username(self, page, username): page.fill(#username, username) step_with_screenshot def click_login(self, page): page.click(#submit)注意page.screenshot()返回的是bytes所以这里用allure.attach是合适的。通常步骤截图用full_pageFalse就够了只截视口减少数据处理量否则每步都整页会非常慢。这里有个容易忽视的细节装饰器函数的第一个参数必须是page。我故意这样设计因为步骤截图本质是“给某个页面操作留证据”必须拿到真实页面实例。如果你想支持更多参数改成*args、**kwargs里捕获page也行但不建议把逻辑搞复杂。3.3 图片处理缩放、JPEG编码、EntirePage截图截图多了之后报告体积会膨胀得很厉害。一张1920x1080的PNG普遍在1MB以上几十个用例就是几十MB。Allure打开时明显变卡CI产物也大。我的处理方式是优先用JPEG代替PNG图像质量损失在可接受范围内对超过一定尺寸的截图先缩放再保存只有“最终失败快照”才用整页截图步骤截图一律视口截图。实际代码像这样from PIL import Image import io def compress_screenshot(page, max_width1280, quality80): raw page.screenshot(full_pageFalse) img Image.open(io.BytesIO(raw)) if img.width max_width: ratio max_width / img.width img img.resize((max_width, int(img.height * ratio))) buffer io.BytesIO() img.convert(RGB).save(buffer, formatJPEG, qualityquality) return buffer.getvalue()用的时候压缩后的图片照样通过allure.attach挂上去。格式这里我习惯用allure.attachment_type.JPEGAllure的网页端对JPEG的兼容性不错还能直接预览。我后来把compress_screenshot放进了conftest.py所有截图统一走这条管道报告体积从之前的40MB降到了6MB左右效果明显。4. 视频录制的完整配置4.1 Playwright的上下文级视频录制Playwright没有直接提供“录制浏览器窗口”的API而是在创建BrowserContext时设置record_video_dir。也就是说每个页面活动都被录进独立的webm文件。这个设计和人脑的直觉不太一样——很多人以为视频是挂在page上的实际上是整个上下文级别的录制。代码如下import pytest from playwright.sync_api import sync_playwright pytest.fixture(scopefunction) def page_with_video(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( record_video_dirassets/videos, record_video_size{width: 1280, height: 720}, ) page context.new_page() yield page context.close() browser.close()重点说说record_video_size。我用的1280x720既保证清晰度又不会让文件无限膨胀。如果你录4K一个用例跑两分钟轻松上百MB不太划算。分辨率也不用和浏览器窗口完全一致Playwright会自动拉伸但比例差别太大会导致画面变形所以建议比例保持一致就好。context.close()这步不能省。视频文件是在context关闭时才真正落盘的如果你只关了page就继续跑别的用例旧context可能保持不关视频一直不生成。我一开始就栽在这里跑完整个suite发现videos目录是空的一度以为API有问题。4.2 如何优雅关闭上下文确保视频落盘上面代码里的context.close()会把当前上下文的视频文件写到record_video_dir。文件命名格式通常是{random-uuid}.webmPlaywright自动起名。如果你想给文件起个有意义的名字官方没直接给参数但在context.close()之前可以通过page.video.path()拿到即将生成的视频路径。拿到路径后可以复制/改名这是我常用的方案video_path page.video.path() # context.close()之后这个路径才真正有内容 context.close() shutil.move(video_path, fassets/videos/{test_name}.webm)要注意的是page.video只有在context设置了record_video_dir时才非空否则访问.video会直接抛异常。所以封装fixture时最好先判断一下。4.3 将视频文件挂载到Allure视频挂载和截图一样终究走的是allure.attach或allure.attach.file。视频文件一般是webm格式Allure对它的支持需要指定attachment_type为WEBM。import allure allure.attach.file( video_path, name测试过程视频, attachment_typeallure.attachment_type.WEBM, )注意allure-pytest的attachment_type枚举里是有WEBM的起码在2.13之后的版本里没问题。旧版本可能没有这个枚举我最早遇到的就是WEBM选不了后来升级库版本才解决。如果你用的版本比较老可以直接传video/webm这个MIME字符串allure.attach.file(video_path, name视频, attachment_typevideo/webm)两种写法在生成报告时效果一致。4.4 保留策略全量录制与仅失败录制全量录制最省事但会消耗大量磁盘空间。反过来只保留失败用例的视频能极大减少存储压力排查效率也不受影响——通过的用例本来就不用回放。我实践的方案是默认全量录制但在report阶段只把失败用例的视频attach进去。实现思路是在conftest里维护当前测试名称的变量然后在钩子里根据失败/成功判断是否attachcurrent_test_video {} pytest.fixture def video_for_test(request): test_name request.node.name yield test_name # 结束后把成功用例的视频删掉 if test_name in current_test_video and not request.node.rep_call.failed: video_path current_test_video.pop(test_name) Path(video_path).unlink(missing_okTrue) pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): ...这个思路不复杂但是能省几十倍的硬盘空间。具体来说video_for_test这个fixture在用例结束时判断是否失败失败的用例会被pytest_runtest_makereport钩子里attach视频通过的则删掉webm文件。如果你的CI磁盘够大想偷懒直接全量attach也不是不行。但从用户体验看报告里全是多余视频反而干扰开发定位问题所以我会比较推荐“失败保留”的策略。5. 将截图和视频写入Allure的标准化写法5.1 allure.attach的注册时机官方文档里allure.attach可以出现在测试函数里、fixture里、甚至是钩子里。注册时机决定了附件挂在哪一层。我踩过几个坑之后总结出的原则是失败截图应该在pytest_runtest_makereport钩子里挂因为这个钩子能拿到最权威的失败状态步骤截图应该在业务代码里挂跟着步骤走内容更有针对性视频建议也在钩子里挂和失败截图同层保持代码集中。钩子里挂附件的优点是不会污染业务代码。业务层只负责操作页面、写断言至于“失败后怎么处理证据”全部集中在conftest里。这样测试代码更简洁新成员接手时也不容易在用例里夹带私货。5.2 处理base64超限问题附件文件化Allure的内部机制把附件先转成base64字符串存在allure-results目录下的json文件里。小附件没什么问题但一个几十MB的视频转成base64会膨胀约33%而且json文件会大到难以解析。这个问题在大型项目中特别明显跑完几百个用例每个用例挂一个大视频最终allure-results目录里躺着几百个几十MB的json文件生成报告时要么卡死要么报错。所以我的硬性要求是所有超过1MB的附件一律走allure.attach.file不要用allure.attach传二进制。截图上文已经用JPEG压缩过了视频本身比较大所以视频必须走attach.file。# 推荐把文件路径交给Allure allure.attach.file( assets/videos/test_abc.webm, name失败视频, attachment_typeallure.attachment_type.WEBM, )不要这样做# 不推荐大文件转bytes再塞给Allure with open(assets/videos/test_abc.webm, rb) as f: allure.attach(f.read(), name视频, attachment_typeallure.attachment_type.WEBM)用attach.file时Allure会在生成报告阶段直接把文件复制到报告目录不走json中转效率和稳定性都好一大截。也许你会问既然attach.file这么好为什么还要保留allure.attach因为attach适合Step内部临时生成的小附件比如接口返回的JSON片段、压缩后只有几十KB的截图。按大小分流就行。5.3 用allure.step组织测试步骤Allure里的allure.step能把测试步骤串成树状结构和截图视频配合起来看排查体验会上升一个台阶。import allure def test_login_success(page): with allure.step(打开登录页): page.goto(https://example.com/login) page.screenshot(pathassets/screenshots/open_login.png) allure.attach.file(assets/screenshots/open_login.png, name打开登录页, attachment_typeallure.attachment_type.PNG) with allure.step(输入用户名密码): page.fill(#username, test_user) page.fill(#password, 123456) with allure.step(点击登录并断言): page.click(#submit) assert 首页 in page.title()如果你用了allure.step推荐把step里的操作配合allure.step装饰器使用这样比较方便。但就截图视频这套方案而言with allure.step已经够用没必要强制封装到装饰器层面。6. 实战一个完整的测试用例拆解6.1 用例场景拿一个登录用例当例子打开网站首页点击登录入口填写账号密码点击登录断言页面跳转成功。过程中我希望记录每一步的关键截图并在失败时自动保存整页截图和视频。这个用例覆盖了三层需求业务层正常的登录操作和断言采集层步骤截图失败整页截图视频录制展示层所有附件在Allure里有层次地展示出来。6.2 conftest.py完整代码先看完整的conftest.py这段代码已经在我实际项目中跑过很多轮稳定性可以放心。import shutil from pathlib import Path import allure import pytest from playwright.sync_api import sync_playwright BASE_DIR Path(__file__).parent SCREENSHOT_DIR BASE_DIR / assets / screenshots VIDEO_DIR BASE_DIR / assets / videos SCREENSHOT_DIR.mkdir(parentsTrue, exist_okTrue) VIDEO_DIR.mkdir(parentsTrue, exist_okTrue) pytest.fixture(scopefunction) def page_with_video(request): test_name request.node.name with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( record_video_dirstr(VIDEO_DIR), record_video_size{width: 1280, height: 720}, ) page context.new_page() yield page # 用例结束后拿到视频文件原始路径 video_path None try: video_path page.video.path() except Exception: pass context.close() browser.close() # 如果有视频且有需要attach的情况改名成可读文件名 if video_path and Path(video_path).exists(): target_path VIDEO_DIR / f{test_name}.webm shutil.move(video_path, target_path) # 临时保存起来供钩子决定是否挂到报告 request.node._video_path str(target_path) pytest.fixture() def video_result(request): return getattr(request.node, _video_path, None) pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call: page item.funcargs.get(page_with_video) # 失败时挂截图 if report.failed and page is not None: shot_path SCREENSHOT_DIR / f{item.name}_failed.png page.screenshot(pathstr(shot_path), full_pageTrue) allure.attach.file( str(shot_path), namef{item.name}失败整页截图, attachment_typeallure.attachment_type.PNG, ) # 失败时同时挂视频 video_path getattr(item.node, _video_path, None) if video_path and Path(video_path).exists(): allure.attach.file( video_path, namef{item.name}测试视频, attachment_typeallure.attachment_type.WEBM, ) # 成功后清理视频避免堆积 if report.passed: video_path getattr(item.node, _video_path, None) if video_path and Path(video_path).exists(): Path(video_path).unlink(missing_okTrue)这里有个关键点page_with_video的fixture在用例结束后已经把视频改名并记录到item.node._video_path了。pytest_runtest_makereport钩子执行时机在fixture teardown之后所以能拿到这个路径。如果你把钩子和fixture的顺序搞反视频还没落盘就查路径只能拿到一个空字符串或None。6.3 测试用例代码测试用例本身不需要关心截图和视频逻辑只需要在关键步骤里主动allure.stepimport allure allure.title(用户登录成功后跳转首页) def test_login_success(page_with_video): with allure.step(打开登录页): page_with_video.goto(https://example.com/login) with allure.step(填写账号密码): page_with_video.fill(#username, tester) page_with_video.fill(#password, 123456) with allure.step(点击登录按钮): page_with_video.click(#submit) with allure.step(断言跳转到首页): assert 首页 in page_with_video.title()如果你想在用例里主动截步骤截图可以加这么一段with allure.step(记录登录页状态): shot_bytes page_with_video.screenshot() allure.attach(shot_bytes, name登录页截图, attachment_typeallure.attachment_type.PNG)6.4 运行与查看报告执行命令pytest -s tests/ --alluredirreports/allure-results allure generate reports/allure-results -o reports/allure-report --clean allure open reports/allure-report打开后每个用例的详情页里就能看到失败截图和视频了。如果历史记录里报告没有更新可能是因为用了--clean但没清理旧的allure-results。我建议每次跑之前先删掉旧目录rm -rf reports/allure-results reports/allure-report7. 常见问题与排查技巧实录7.1 报告里看不到附件这个问题我遇到过不下三次每次都花了些时间。原因基本是三选一allure.attach和allure.attach.file没有被执行到比如钩子里的条件判断没匹配上附件文件路径不对Allure找不到文件allure-pytest版本问题导致attachment类型不识别。排查技巧很粗暴但有效先跑一条用例打开reports/allure-results目录下的json文件手动搜一下attachments字段里有没有对应的文件名。如果json里压根没有附件字段说明attach没执行如果有字段但生成报告时不显示再去查文件路径和类型。另外检查你的allure命令行工具和allure-pytest库是否匹配。我建议命令行用2.24以上allure-pytest用2.13以上这两者混用过老版本时出现过附件不展示的情况。7.2 视频文件为0字节视频文件是0字节通常是因为context.close()之前就访问了文件或者录制还没真正结束视频内容没刷到磁盘。我最初踩的坑就是只在用例最后调用context.close()但钩子里的page.screenshot在close之后执行导致视频不完整。正确的顺序是先让用例跑完然后立即context.close()确保视频落盘之后再处理附件和清理逻辑。如果你需要先截图再保存视频就要在截图之前把page.video.path()存好等context.close()后再操作文件。7.3 截图上全是空白页面没加载完就截图这是最常见的原因。Playwright的page.goto()默认等待load事件但有些页面是异步渲染的load之后DOM还在变。截图前先等等关键元素page.goto(https://example.com/login) page.wait_for_selector(#login-form) # 等关键元素出现另一个原因是headless模式下GPU渲染问题个别页面截图会白屏。如果你遇到这种鬼畜问题可以尝试启动参数加--disable-gpubrowser p.chromium.launch(headlessTrue, args[--disable-gpu])不出意外多数白屏问题都能解决。7.4 多进程/分布式执行时附件混乱pytest-xdist并行跑的时候多个进程同时往assets/screenshots和assets/videos写文件同名文件会互相覆盖而且Allure附件会串。这个问题很恶心。我的解决方案是给每个worker独立目录。用pytest的worker_id区分import os WORKER_ID os.environ.get(PYTEST_XDIST_WORKER, master) SCREENSHOT_DIR BASE_DIR / assets / fscreenshots_{WORKER_ID} VIDEO_DIR BASE_DIR / assets / fvideos_{WORKER_ID}这样每个worker各写各的最后Allure生成报告时可以正常聚合。唯一要注意的是跑完后清理目录时要把这些带后缀的目录一并清掉。7.5 Allure打开慢报告打开慢基本是附件文件夹里堆了大文件。除了上文的JPEG压缩你还可以视频统一改用更短、更低码率的录制参数在Allure配置里设置不自动加载附件预览按需清理历史allure-results别让磁盘上堆太多。以下是我项目里用的优化后录制参数context browser.new_context( record_video_dirstr(VIDEO_DIR), record_video_size{width: 1280, height: 720}, )Playwright录webm的码率没法直接调能控制的就是分辨率和视频时长。让用例尽量短、步骤尽量精简是最有效的降体积手段。8. 几个提升效率的后续扩展8.1 重试与失败重跑的附件合并如果用例重试后通过了你肯定不想让一份失败截图永远留在报告里。Allure有--retries机制重跑后的结果会合并。我通常配合pytest-rerunfailures使用pytest tests/ --reruns 2 --alluredirreports/allure-results重试产生的旧附件会在新结果中标注为“重试”报告里默认展示最新的一次。你可以在conftest.py里针对重试情况做判断只保留最终失败的那次截图视频。8.2 diff截图断言除了失败截图你还可以做“视觉回归”截图。把关键页面截图存成基线每次执行后和基线做diff差异超过阈值就自动挂到Allure里。方法很多我常用的是pixelmatch这个Python库它对PNG的比对效果不错from pixelmatch.contrib.PIL import compare_images diff compare_images(base_img, current_img, threshold0.1)然后把diff图通过allure.attach.file挂上去。加上这个能力后UI自动化几乎能覆盖到所有页面的视觉变化比单纯断言文案强大很多。8.3 历史记录趋势图的保留Allure的历史趋势图默认只记录最近几次执行。如果你希望保留更长时间只需要把上次生成的history目录复制到本次report目录下即可。在CI脚本里加一步cp -r reports/allure-report/history reports/allure-results/history allure generate reports/allure-results -o reports/allure-report --clean这样趋势图能连续保留团队看历史稳定性方便不少。视频和截图同样会保留在这套体系里老问题复现时翻几轮历史报告就能找到当时的现场。我自己在很长时间里都低估了截图和视频的价值直到有一次线上故障回查时靠着两周前报告里的视频确认了一个偶现问题跟时序有关才真正觉得这半天改造值了。如果你也开始往报告里加视频和截图建议先只做“失败自动截图失败视频保留”跑通之后再逐步加步骤截图和视觉diff别一口气上太多反而难排查。毕竟稳定的报告体系也是一步步迭代出来的而不是一把梭出来的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑