1. 项目概述为什么选择Playwright来升级我们的自动化测试平台如果你正在搭建或维护一个基于Django和Vue的自动化测试平台并且之前可能用过Selenium、Puppeteer之类的工具那么你肯定对Web UI自动化的痛点深有体会浏览器驱动版本管理、元素定位不稳定、执行速度慢、跨浏览器测试的复杂性……这些问题在项目迭代中会不断消耗团队的精力。我最近就在负责的一个测试平台项目中把Web UI自动化模块从传统的Selenium方案全面升级到了基于Playwright的方案。这个决定不是一时兴起而是经过深度对比和实际压测后做出的。Playwright是微软开源的一个现代化Web自动化测试框架它原生支持Chromium、Firefox和WebKit三大浏览器引擎这意味着你写一套脚本就能在Chrome、Edge、Safari和Firefox上运行。更重要的是它采用了一种更“聪明”的架构通过WebSocket协议与浏览器通信能直接监听浏览器内部事件而不是像Selenium那样依赖HTTP轮询。这带来的直接好处就是执行速度更快、更稳定对动态内容的等待也更智能。在我们的测试平台里这意味着更短的测试执行时间、更低的资源占用以及更少的“Flaky Tests”那些时好时坏的测试用例。这次升级的核心目标不仅仅是换一个底层驱动而是构建一个更健壮、更易维护、功能更丰富的Web UI自动化能力。我们将把它深度集成到现有的Django后端负责测试用例管理、任务调度、结果存储和Vue前端负责可视化操作和报告展示中。最终测试工程师可以在平台上通过简单的配置就完成复杂的Web UI自动化场景编排、执行和结果分析。2. 平台架构升级从Selenium到Playwright的平滑迁移设计2.1 原有架构痛点分析与新架构选型在升级之前我们的平台使用的是Selenium Grid 各语言绑定主要是Python的方案。这套方案运行了两年暴露了几个核心问题稳定性依赖驱动匹配每个测试节点都需要严格匹配浏览器版本和对应的WebDriver版本升级是运维噩梦。执行效率瓶颈基于HTTP的通信方式在复杂页面操作时延迟明显特别是需要等待大量AJAX或前端框架如Vue/React渲染完成时。多浏览器支持成本高要稳定运行Firefox和Safari测试需要额外的配置和调试且行为一致性难以保证。高级特性缺失模拟移动设备、网络拦截、下载文件、录制视频等高级功能需要引入大量第三方库或复杂代码。Playwright的出现几乎是为解决这些问题而生的。它的设计哲学是“一个API所有浏览器”。我们选择它主要基于以下几点架构优势浏览器引擎内置playwright install命令会自动下载匹配的、经过测试的浏览器二进制文件彻底摆脱了驱动管理的困扰。这些浏览器被封装在独立的用户目录下与系统浏览器隔离保证了测试环境的纯净性。自动等待机制Playwright的大部分操作如click,fill都内置了智能等待。它会等待元素可操作可见、启用、稳定后才执行这极大地减少了测试脚本中显式添加sleep或复杂等待逻辑的需要让脚本更简洁、更健壮。网络控制能力可以直接拦截和修改网络请求这对于测试需要模拟不同网络环境如弱网、Mock API接口返回、或者跳过某些第三方资源加载以加速测试场景非常有用。丰富的上下文Context和页面Page管理可以轻松创建独立的浏览器上下文相当于无痕会话用于模拟多用户登录也可以在一个上下文中打开多个标签页Page进行并行操作资源利用率更高。在我们的新架构中Django后端将作为“测试大脑”和“数据中心”。它负责存储和管理用YAML或JSON描述的测试场景包含步骤、元素定位器、断言等。接收前端发起的测试执行请求将任务放入队列我们使用Celery Redis。调度Celery Worker运行在独立的测试执行节点上去执行具体的Playwright脚本。接收Worker返回的原始结果截图、视频、执行日志、通过/失败状态进行结构化处理并存入数据库PostgreSQL。Vue前端则提供友好的交互界面让用户能够通过“录制”或“配置”的方式可视化地编排测试步骤。选择在哪种浏览器、哪种设备如iPhone 13上运行测试。实时查看测试执行日志和视频回放。通过丰富的图表分析历史测试结果和趋势。2.2 技术栈整合与依赖管理升级意味着我们要引入新的依赖并调整项目结构。以下是核心的技术栈调整后端 (Django) 关键依赖# requirements.txt 新增 playwright1.40.0 # 核心自动化库 celery5.3.4 # 异步任务队列 redis4.5.4 # Celery消息代理和结果后端 django-celery-results2.5.0 # 将Celery结果存储到Django数据库 psycopg2-binary2.9.9 # PostgreSQL驱动用于存储结构化结果前端 (Vue) 关键依赖前端主要变化在于新增了与测试编排和报告展示相关的组件。我们使用了Element Plus作为UI框架并通过Axios与Django REST Framework API通信。一个重要的新增功能是“测试步骤编辑器”它允许用户通过拖拽或表单填写来定义操作序列。执行节点 (Celery Worker) 环境准备每个执行节点可以是物理机、虚拟机或Docker容器都需要安装Playwright的浏览器。我们通过一个启动脚本或Dockerfile来标准化环境。# Dockerfile 示例片段 FROM python:3.10-slim RUN apt-get update apt-get install -y --no-install-recommends \ wget \ gnupg \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 关键步骤安装Playwright及其浏览器 RUN playwright install --with-deps chromium firefox webkit # 安装中文字体确保截图和渲染正常 RUN apt-get update apt-get install -y fonts-wqy-zenhei注意playwright install --with-deps会安装浏览器以及其所需的系统依赖如库文件这在Linux环境下尤其重要可以避免运行时出现“缺少共享库”的错误。3. 核心模块实现Playwright能力在平台中的深度集成3.1 测试用例的数据模型与剧本设计在Django中我们需要设计数据模型来抽象一个Web UI测试用例。它不再是简单的脚本文件而是一个结构化的“剧本”。# models.py from django.db import models class WebUITestCase(models.Model): name models.CharField(max_length255) description models.TextField(blankTrue) project models.ForeignKey(Project, on_deletemodels.CASCADE) created_by models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) created_at models.DateTimeField(auto_now_addTrue) # 执行配置 browser models.CharField(max_length50, choices[(chromium, Chromium), (firefox, Firefox), (webkit, WebKit)], defaultchromium) device models.CharField(max_length100, blankTrue, help_texte.g., iPhone 13 Pro Max) # 模拟设备 viewport models.JSONField(defaultdict, blankTrue, help_text{width: 1920, height: 1080}) headless models.BooleanField(defaultTrue) # 是否无头模式运行 class TestStep(models.Model): test_case models.ForeignKey(WebUITestCase, on_deletemodels.CASCADE, related_namesteps) order models.IntegerField() # 步骤顺序 action models.CharField(max_length100) # 操作类型如 goto, click, fill, assert selector models.TextField(blankTrue) # 元素定位器支持多种格式 value models.TextField(blankTrue) # 操作值如URL、输入文本、预期结果 options models.JSONField(defaultdict, blankTrue) # 额外选项如等待时间、截图设置一个测试用例对应多个有序的TestStep。selector字段我们设计为支持Playwright丰富的定位器语法比如text登录匹配包含“登录”文本的元素。#username匹配ID为username的元素。input[nameemail]匹配name属性为email的input元素。cssnav text首页在nav元素内查找文本为“首页”的元素。这种设计使得测试用例可以通过前端界面进行可视化编排和修改而无需直接编写代码。3.2 异步任务执行引擎Celery Worker的实现这是平台的核心执行引擎。每个Worker都是一个独立的进程从Redis队列中获取任务执行Playwright脚本并返回结果。# tasks.py from celery import shared_task from django.core.files.base import ContentFile from .models import WebUITestCase, TestExecution import asyncio from playwright.async_api import async_playwright import json import os from datetime import datetime shared_task(bindTrue, max_retries3) def execute_webui_test(self, test_case_id, execution_id): 执行Web UI测试的Celery任务 test_case WebUITestCase.objects.get(idtest_case_id) execution TestExecution.objects.get(idexecution_id) execution.status RUNNING execution.started_at datetime.now() execution.save() # 准备执行结果目录 result_dir f/tmp/test_results/{execution_id} os.makedirs(result_dir, exist_okTrue) async def run_test(): async with async_playwright() as p: # 1. 启动浏览器根据配置选择 launch_options {headless: test_case.headless} if test_case.device: # 使用Playwright内置的设备模拟 device p.devices.get(test_case.device) if device: launch_options.update(device) browser await getattr(p, test_case.browser).launch(**launch_options) # 2. 创建浏览器上下文Context可以设置视口、语言、地理位置等 context await browser.new_context( viewporttest_case.viewport if test_case.viewport else {width: 1920, height: 1080}, localezh-CN, # 设置中文环境 record_video_dirresult_dir if test_case.record_video else None # 录制视频 ) page await context.new_page() execution_logs [] try: # 3. 按顺序执行测试步骤 steps test_case.steps.all().order_by(order) for step in steps: log_entry {step: step.order, action: step.action, timestamp: datetime.now().isoformat()} try: if step.action goto: await page.goto(step.value, timeout60000) log_entry[info] f导航到: {step.value} elif step.action click: await page.click(step.selector, timeout10000) log_entry[info] f点击元素: {step.selector} elif step.action fill: await page.fill(step.selector, step.value) log_entry[info] f在 {step.selector} 中输入: {step.value} elif step.action assert_text: actual_text await page.text_content(step.selector) if step.value in actual_text: log_entry[info] f断言成功找到文本: {step.value} else: log_entry[error] f断言失败。期望包含“{step.value}”实际为“{actual_text}” raise AssertionError(log_entry[error]) # ... 处理更多 action 类型 # 步骤执行后根据配置决定是否截图 if step.options.get(screenshot): screenshot_path os.path.join(result_dir, fstep_{step.order}.png) await page.screenshot(pathscreenshot_path, full_pagestep.options.get(full_page, False)) log_entry[screenshot] screenshot_path except Exception as e: log_entry[error] str(e) # 出错时自动截图便于排查 error_screenshot os.path.join(result_dir, ferror_step_{step.order}.png) await page.screenshot(patherror_screenshot, full_pageTrue) log_entry[error_screenshot] error_screenshot raise e # 抛出异常终止测试 finally: execution_logs.append(log_entry) # 所有步骤成功完成 execution.status PASSED execution_logs.append({info: 所有测试步骤执行通过, timestamp: datetime.now().isoformat()}) except Exception as e: execution.status FAILED execution_logs.append({error: f测试执行失败: {str(e)}, timestamp: datetime.now().isoformat()}) self.retry(exce, countdown60) if self.request.retries self.max_retries else None # 失败重试 finally: # 4. 关闭资源 await context.close() await browser.close() # 5. 保存执行结果 execution.ended_at datetime.now() execution.logs json.dumps(execution_logs, ensure_asciiFalse, indent2) # 如果有视频保存视频路径 if test_case.record_video: # 视频文件在context关闭后才会最终生成 video_path await page.video.path() if page.video else None if video_path and os.path.exists(video_path): with open(video_path, rb) as f: execution.video.save(fexecution_{execution_id}.webm, ContentFile(f.read())) execution.save() # 在Celery任务中运行异步函数 asyncio.run(run_test()) return {status: execution.status, execution_id: execution_id}这个execute_webui_test任务是一个完整的执行单元。它展示了几个关键实践异步执行使用async/await充分利用Playwright的异步API提升单个Worker执行多个测试用例或步骤的并发能力。资源隔离每个测试用例在独立的BrowserContext中运行确保Cookie、LocalStorage等状态不会互相污染。完善的日志和截图每一步操作都有日志记录出错时自动截取全屏图为问题排查提供了最直接的证据。失败重试机制通过Celery的retry机制对因网络抖动等临时性问题导致的失败进行自动重试提高测试稳定性。3.3 前端测试编排与报告展示Vue前端需要提供两个核心页面测试用例编排器和测试报告详情页。测试用例编排器 我们实现了一个可拖拽的步骤列表。用户可以从左侧的操作面板包含“打开网页”、“点击”、“输入”、“断言”等拖拽到中间的画布形成一个步骤流。每个步骤可以右侧面板编辑其详细信息如URL、定位器、输入值等。一个高级功能是集成Playwright的“代码录制”功能用户点击“开始录制”平台会在后端启动一个无头浏览器并打开指定URL同时生成一个唯一的WebSocket连接地址。前端通过这个连接打开一个浏览器窗口用户的实际操作会被实时录制并转化为测试步骤添加到画布中。这极大地降低了编写复杂测试用例的门槛。测试报告详情页 当测试执行完成后前端通过WebSocket或轮询从后端获取实时日志并展示一个时间线。报告页清晰地分为几个区域概览通过/失败状态、总耗时、浏览器/设备信息。步骤时间线以卡片形式展示每个步骤的开始结束时间、状态成功/失败、日志和关联的截图。失败的步骤会高亮显示。媒体区如果测试录制了视频这里会嵌入一个视频播放器支持慢放和跳转到失败时间点。每一步的截图也可以点击放大查看。控制台日志展示原始的、结构化的执行日志方便高级用户深入排查。4. 高级特性与性能优化实战4.1 利用Playwright Context实现高效的多场景测试Playwright的BrowserContext是一个强大的抽象它代表了一个独立的浏览器会话。在我们的平台中我们利用它来实现两个高级场景场景一并行登录测试假设我们需要测试一个电商网站不同用户角色的功能。我们可以创建多个Context每个Context使用不同的Cookie或本地存储状态来模拟已登录的不同用户如普通用户、管理员然后在这些Context中并行执行各自的测试流。这比串行登录-退出-再登录高效得多。async def test_parallel_users(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) # 模拟用户A管理员 context_a await browser.new_context() await context_a.add_cookies([{name: session, value: admin_token, url: https://example.com}]) page_a await context_a.new_page() # 模拟用户B普通用户 context_b await browser.new_context() await context_b.add_cookies([{name: session, value: user_token, url: https://example.com}]) page_b await context_b.new_page() # 现在可以并行操作page_a和page_b它们互不干扰 await asyncio.gather( admin_test_flow(page_a), user_test_flow(page_b) )场景二设备与视口模拟测试响应式布局平台允许用户在创建测试用例时选择设备型号如iPhone 13。后端在执行时会从Playwright内置的设备列表p.devices中获取该设备的配置包括视口大小、User-Agent、是否是移动端等并应用到new_context中。这样一次编写即可在多种设备视口下验证UI表现。4.2 网络拦截与Mock提升测试稳定性和速度这是Playwright相比Selenium的一大杀手锏。我们可以拦截请求实现屏蔽非必要资源在测试环境中可以拦截对广告、分析脚本、第三方字体等资源的请求直接返回空响应从而大幅加快页面加载速度。Mock API接口对于依赖后端API的前端测试我们可以拦截特定的API请求并返回预设的Mock数据。这使我们可以测试前端在各种数据状态下的表现而不受后端开发进度或数据稳定性的影响。模拟网络条件可以模拟慢速3G、离线等网络环境测试应用的健壮性。# 在创建page或context时设置路由 async def test_with_mock(): async with async_playwright() as p: browser await p.chromium.launch() context await browser.new_context() page await context.new_page() # 拦截并Mock一个API请求 await page.route(**/api/user/profile, lambda route: route.fulfill( status200, content_typeapplication/json, bodyjson.dumps({name: Mock User, role: admin}) )) # 拦截并中止对图片资源的请求加速测试 await page.route(**/*.{png,jpg,jpeg}, lambda route: route.abort()) await page.goto(https://example.com) # 此时页面加载的/api/user/profile将收到Mock数据且不会加载图片4.3 执行性能优化与资源管理当平台需要同时执行大量UI测试时资源管理和性能成为关键。浏览器实例复用频繁启动和关闭浏览器开销巨大。我们实现了一个简单的“浏览器池”。Celery Worker启动时可以预启动一个无头浏览器实例并将其放入一个线程安全的队列中。当有测试任务到来时从池中取出一个浏览器实例来创建新的Context执行测试执行完毕后关闭Context但浏览器实例放回池中供后续使用。这避免了每次测试都启动浏览器的开销。测试用例并行化一个测试用例内的步骤通常是串行的但平台可以并行执行多个独立的测试用例。我们通过配置Celery的并发Worker数量celery -A proj worker --concurrency4来实现。每个Worker可以同时处理一个任务。结合浏览器池可以高效利用多核CPU。智能等待策略虽然Playwright有自动等待但对于某些自定义加载组件我们可以在平台层面封装更高级的等待方法。例如等待某个Vue组件的加载动画消失通过判断特定的CSS类或元素出现然后再进行下一步操作。我们将这些通用等待策略封装成平台支持的“自定义步骤类型”。结果存储优化截图和视频文件很大。我们不会将它们直接存入数据库如PostgreSQL的BYTEA字段而是使用对象存储服务如MinIO、AWS S3或阿里云OSS。数据库中只存储文件的URL路径。对于大量重复的测试可以考虑只存储失败用例的截图和视频或者定期清理历史文件。5. 部署、监控与踩坑实录5.1 基于Docker的标准化部署为了确保测试环境的一致性我们强烈推荐使用Docker部署执行节点Celery Worker。下面是一个精简的Dockerfile示例它包含了所有必需的系统依赖和浏览器。# 使用带有完整系统依赖的Python镜像作为基础比slim版更省心 FROM python:3.10-bullseye WORKDIR /app # 安装系统依赖包括Playwright所需的库和中文字体 RUN apt-get update apt-get install -y --no-install-recommends \ wget \ gnupg \ libnss3 \ libnspr4 \ libatk1.0-0 \ libatk-bridge2.0-0 \ libcups2 \ libdrm2 \ libdbus-1-3 \ libxcb1 \ libxkbcommon0 \ libx11-6 \ libxcomposite1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxrandr2 \ libgbm1 \ libpango-1.0-0 \ libcairo2 \ libasound2 \ fonts-wqy-zenhei \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 安装Playwright和浏览器Chromium, Firefox, WebKit RUN playwright install --with-deps chromium firefox webkit # 复制应用代码 COPY . . # 启动Celery Worker CMD [celery, -A, your_project, worker, --loglevelinfo, --concurrency4]使用Docker Compose可以轻松编排整个平台version: 3.8 services: db: image: postgres:15 environment: POSTGRES_DB: test_platform POSTGRES_USER: user POSTGRES_PASSWORD: password volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7-alpine web: build: ./backend command: python manage.py runserver 0.0.0.0:8000 volumes: - ./backend:/app - media_volume:/app/media # 用于存储上传的文件 - static_volume:/app/static # 收集的静态文件 depends_on: - db - redis celery_worker: build: ./backend command: celery -A your_project worker --loglevelinfo --concurrency4 volumes: - ./backend:/app - media_volume:/app/media - test_results_volume:/tmp/test_results # 挂载测试结果目录 depends_on: - redis - web nginx: image: nginx:alpine ports: - 80:80 volumes: - ./nginx.conf:/etc/nginx/nginx.conf - static_volume:/static - media_volume:/media depends_on: - web volumes: postgres_data: static_volume: media_volume: test_results_volume:5.2 平台监控与告警一个稳定的测试平台离不开监控。Celery监控使用Flower来监控Celery Worker的状态、任务队列和执行情况。它可以展示任务历史、Worker负载、甚至重试情况是排查任务积压或Worker异常的第一站。资源监控对执行节点的CPU、内存、磁盘IO进行监控。UI自动化测试尤其是并行执行时是资源消耗大户。我们设置了告警阈值当节点负载持续过高时触发告警提示可能需要扩容或优化测试用例。测试健康度监控平台定期分析测试结果。我们关注几个核心指标失败率整体或单个项目的测试失败率是否突然升高。平均执行时间测试用例的执行时间是否出现异常增长可能意味着页面性能下降或测试步骤变慢。Flaky Tests识别那些时而成功时而失败的测试用例。平台会标记这些用例并建议维护者进行修复或将其隔离。日志聚合将所有组件Django, Celery, Playwright脚本的日志统一收集到ELKElasticsearch, Logstash, Kibana或Graylog中方便进行全局问题追踪。5.3 常见问题与排查技巧实录在实际开发和运维中我们遇到了不少坑这里分享一些典型的排查经验问题1在Docker中运行Playwright截图或录屏失败提示“Failed to launch because of missing dependencies”原因虽然使用了--with-deps但某些Linux发行版特别是Alpine可能缺少更底层的图形库。解决放弃使用超精简的alpine或slim镜像作为基础改用bullseye或buster等完整版。如果必须用Alpine需要安装大量额外的包且可能仍有兼容性问题不推荐。问题2元素定位失败但手动打开浏览器又能看到排查步骤检查等待首先确认是否给了页面足够的时间加载。虽然Playwright有自动等待但对于某些极端动态的内容可能需要加上page.wait_for_selector(selector, statevisible)。检查iframe元素是否在iframe内部如果是需要先定位到iframeframe page.frame(frame-name-or-selector)然后在frame对象上进行操作。检查Shadow DOM现代前端框架如某些Vue/React组件可能使用Shadow DOM。Playwright支持page.locator(div).shadow_root.locator(button)这样的语法来穿透Shadow DOM。使用更稳定的定位器优先使用get_by_role(),get_by_text(),get_by_label()这些基于可访问性属性的定位器它们比纯CSS或XPath更稳定。避免使用可能变化的类名或索引。启用调试和截图在定位器前后添加screenshot和pause()在非无头模式下运行直观地观察页面状态。问题3测试在CI/CD流水线如GitLab CI, Jenkins中不稳定时好时坏原因CI环境资源CPU、内存通常受限且网络可能不稳定。优化增加超时时间将page.goto,page.click等操作的默认超时时间从30秒提高到60秒甚至更长。使用更稳定的定位器同上。禁用不必要的功能在CI中运行确保使用无头模式headlessTrue并可以考虑禁用GPU加速和沙箱args[--disable-gpu, --no-sandbox]但这会降低安全性仅用于受控的测试环境。实施重试机制在平台层面Celery任务和测试步骤层面使用playwright.sync_api的retry装饰器或自己封装重试逻辑都加入重试。隔离测试数据确保每个测试用例使用独立的数据避免并行执行时的数据竞争。问题4执行大量测试后内存持续增长不释放原因Context或Page没有正确关闭。即使使用async with在异常情况下也可能导致资源泄漏。解决确保在finally块中显式且正确地关闭资源。使用我们上面任务示例中的模式将browser.close()和context.close()放在finally中。同时定期重启Celery Worker进程可以作为一个兜底的清理策略。问题5如何管理测试数据如用户账号最佳实践绝对不要在测试脚本中硬编码真实用户凭证。我们采用以下策略在Django后台管理界面维护一个“测试账户池”。每个测试任务开始前从池中“借用”一个账户标记为使用中。测试脚本通过环境变量或API从平台获取该账户的临时凭证。测试结束后无论成功与否在finally块中调用平台API“归还”账户重置为未使用状态。对于需要特定状态的测试如已下单用户通过平台的“数据准备”接口在测试前调用后端API将账户置为所需状态。从Selenium迁移到Playwright并以此为核心构建自动化测试平台是一次显著的技术升级。它不仅带来了执行速度和稳定性的提升其丰富的API网络拦截、设备模拟、上下文隔离更为我们设计更复杂、更真实的测试场景打开了新的大门。将Playwright与Django、Vue、Celery等技术栈深度集成最终打造出的平台让测试人员从繁琐的环境配置和脚本调试中解放出来更专注于测试用例的设计和业务逻辑的验证。这个过程虽然充满挑战但看到平台稳定运行测试效率大幅提升时一切都值得了。如果你也在规划类似的升级我的建议是从小范围试点开始用一两个核心业务流验证Playwright的稳定性和收益再逐步推广到全平台。