基于Playwright与BDD的ILLA Builder端到端测试实践指南

📅 2026/6/30 6:49:00
基于Playwright与BDD的ILLA Builder端到端测试实践指南
1. 项目概述为什么我们需要为ILLA Builder构建E2E测试如果你和我一样是一个长期与低代码平台打交道的开发者那你一定对ILLA Builder不陌生。它让前端应用的构建变得像搭积木一样直观拖拽组件、配置数据源、绑定事件一个功能丰富的管理后台可能半天就能成型。但问题也随之而来当你的应用逻辑越来越复杂组件交互越来越多如何保证每一次功能迭代后之前跑得好好的流程不会突然“暴毙”靠人工一遍遍点那太不现实了。这就是我今天想聊的——如何用Playwright为ILLA Builder搭建一套坚实、高效的行为驱动开发BDD端到端E2E测试体系。简单来说这个实践的核心目标就一个让测试代码像产品需求文档一样清晰可读并且能自动执行确保ILLA构建的应用质量可控。行为驱动开发BDD在这里扮演了关键角色它强调用自然语言描述用户行为Given-When-Then让产品、开发和测试都能在同一个“频道”上沟通。而Playwright作为微软出品的现代浏览器自动化框架以其跨浏览器支持、自动等待、强大的选择器和追踪功能成为了实现BDD式E2E测试的绝佳工具。这套方案适合谁我认为有三类人最需要一是ILLA Builder的重度使用者需要为自己的项目建立质量护栏二是全栈或测试开发工程师希望将低代码平台的测试也纳入CI/CD流水线三是任何对自动化测试和提升交付信心感兴趣的开发者。接下来我会把我从零搭建这套体系的完整过程、踩过的坑和实战心得毫无保留地分享给你。2. 核心思路与架构设计当BDD遇见Playwright在动手写第一行测试代码之前理清思路至关重要。我们的目标不是写一堆脆弱的、只能自己看懂的脚本而是构建一个可维护、可扩展、能真实反映用户行为的测试套件。2.1 为什么是BDD Playwright首先我们得明确为什么选择这个组合。对于ILLA Builder这类可视化开发工具其产出物是动态的Web应用。测试的重点在于用户与界面交互的完整流程比如“给定一个已登录的管理员当他在表格中筛选状态为‘进行中’的任务然后点击‘导出’按钮那么应该成功下载一个包含筛选结果的CSV文件。” 这种描述本身就是BDD的典型场景。Playwright的优势在于它能完美支撑这种场景自动等待它内置了智能等待机制无需手动添加sleep能自动等待元素可操作、网络请求完成这对ILLA生成的动态内容尤其友好直接避开了“录制脚本最常见的失败原因——动态内容”。多浏览器支持一套脚本可在Chromium、Firefox、WebKit上运行确保跨浏览器一致性。强大的选择器引擎除了常规的CSS、XPath还支持根据文本内容、元素角色Role进行定位这对于测试那些可能没有稳定ID的低代码生成组件非常关键。丰富的调试工具playwright trace可以录制测试过程的完整上下文DOM快照、网络请求、控制台日志任何失败都能快速定位根因。2.2 测试框架选型与项目结构我们不会裸用Playwright的API而是需要一个测试运行器和BDD层。常见的组合是Jest / Vitest playwright/testcucumber-js这是一个功能强大的组合但配置稍显复杂。playwright/test原生支持Playwright Test本身就是一个测试运行器且社区有cucumber/playwright等适配方案。为了追求简洁和更好的集成度我选择了Playwright Test原生运行器并搭配playwright/bdd插件。这是Playwright官方推荐的BDD集成方式无需额外配置运行器语法也更统一。一个清晰的项目结构是维护性的基石。我的项目目录通常如下所示e2e-illa-tests/ ├── package.json ├── playwright.config.ts # Playwright 主配置 ├── bdd.config.ts # BDD 特性文件配置可选 ├── features/ # BDD 特性描述文件 │ ├── login.feature │ ├── data_crud.feature │ └── dashboard.feature ├── steps/ # 步骤定义实现 │ ├── login.steps.ts │ ├── common.steps.ts │ └── data_crud.steps.ts ├── pages/ # 页面对象模型POM用于封装ILLA页面元素 │ ├── LoginPage.ts │ ├── BuilderCanvas.ts │ └── AppViewerPage.ts ├── fixtures/ # 测试夹具如测试数据、通用配置 │ └── test-data.ts ├── utils/ # 工具函数 │ └── helpers.ts └── tests/ # 传统的Playwright测试文件如有需要也可保留 └── example.spec.ts设计考量采用features/存放用Gherkin语法写的.feature文件这是业务方和测试人员都能读懂的“需求文档”。steps/目录下的文件将Gherkin语句映射到具体的Playwright操作。pages/目录实现了页面对象模式将ILLA Builder的界面元素如组件、按钮、输入框封装起来避免选择器散落在步骤文件中极大提升了代码的可维护性和复用性。3. 环境搭建与核心配置详解工欲善其事必先利其器。稳定的环境是自动化测试的基石。3.1 初始化项目与依赖安装首先创建一个新目录并初始化npm项目。mkdir illa-builder-e2e cd illa-builder-e2e npm init -y接着安装核心依赖。这里我们安装Playwright Test以及其BDD插件。npm install --save-dev playwright/test npm install --save-dev playwright/bdd注意安装Playwright浏览器时如果遇到playwright install chromium速度很慢的问题可以通过设置环境变量来使用国内镜像源加速这是一个非常实用的技巧。# 在安装前设置镜像源以阿里云为例具体镜像地址请查询最新可用源 set PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright npx playwright install chromium # 或者一次性安装所有浏览器 npx playwright install3.2 Playwright与BDD配置接下来是关键的配置文件playwright.config.ts。这里我们需要集成BDD插件。import { defineConfig, devices } from playwright/test; import { defineBddConfig } from playwright/bdd; const testDir defineBddConfig({ paths: [features/*.feature], // 指定特性文件路径 require: [steps/*.ts], // 指定步骤定义文件路径 importTestFrom: fixtures/fixtures.ts, // 从fixtures导入测试实例 }); export default defineConfig({ testDir: ./, // BDD插件会处理这里可以指向根目录或特定目录 ...testDir, // 展开BDD配置 fullyParallel: true, // 完全并行执行测试 forbidOnly: !!process.env.CI, // 在CI环境中禁止使用test.only retries: process.env.CI ? 2 : 1, // CI环境重试2次本地重试1次 workers: process.env.CI ? 4 : undefined, // CI环境使用4个worker并行 reporter: [ [html, { outputFolder: playwright-report }], // HTML报告 [list], // 控制台列表输出 [json, { outputFile: test-results.json }] // JSON报告用于CI集成 ], use: { baseURL: http://localhost:3000, // 你的ILLA应用地址可通过环境变量覆盖 trace: on-first-retry, // 仅在第一次重试时记录trace平衡性能与调试 screenshot: only-on-failure, // 仅在失败时截图 video: retain-on-failure, // 仅在失败时保留录像 actionTimeout: 15000, // 单个操作超时时间 navigationTimeout: 30000, // 导航超时时间 }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] }, }, // 可以按需添加其他浏览器项目 // { // name: firefox, // use: { ...devices[Desktop Firefox] }, // }, ], webServer: { command: npm run start, // 假设你的ILLA应用通过此命令启动 url: http://localhost:3000, reuseExistingServer: !process.env.CI, // CI环境不重用现有服务器 timeout: 120 * 1000, // 服务器启动超时时间 }, });配置要点解析testDir与BDD集成通过defineBddConfig将Gherkin特性文件与步骤定义关联起来。fixtures我创建了一个fixtures.ts文件用于导出增强了BDD步骤的test对象。这是playwright/bdd推荐的做法它允许你在步骤定义中直接使用test实例。// fixtures/fixtures.ts import { test as base } from playwright/test; import { createBdd } from playwright/bdd; const { Given, When, Then } createBdd(base); export { Given, When, Then }; export const test base;超时设置ILLA应用可能涉及复杂的数据加载和渲染适当调高actionTimeout和navigationTimeout是必要的。Web Server配置webServer选项非常有用它允许Playwright在运行测试前自动启动你的应用测试结束后自动关闭实现了真正的“一键测试”。4. 编写BDD特性与步骤定义这是将业务语言转化为自动化脚本的核心环节。4.1 编写Gherkin特性文件在features/login.feature中我们用自然语言描述一个登录场景。Feature: 用户登录 作为ILLA Builder的应用管理员 我希望能够安全地登录到系统 以便管理和构建我的应用 Scenario: 使用有效凭据成功登录 Given 我打开了ILLA登录页面 When 我在用户名输入框中输入 admin And 我在密码输入框中输入 correct_password And 我点击登录按钮 Then 我应该被重定向到应用管理面板 And 我应该能看到用户头像显示 admin Scenario: 使用无效密码登录失败 Given 我打开了ILLA登录页面 When 我在用户名输入框中输入 admin And 我在密码输入框中输入 wrong_password And 我点击登录按钮 Then 我应该看到错误提示信息 用户名或密码错误 And 我应该仍然停留在登录页面要点Feature描述功能价值Scenario描述具体用例。步骤使用Given前置条件、When操作、Then断言关键字And用于连接同类型步骤。语言要简洁、无歧义。4.2 实现步骤定义与页面对象接下来在steps/login.steps.ts中实现这些步骤。这里我们会用到页面对象模型POM。首先创建页面对象pages/LoginPage.ts封装登录页面的元素和操作。import { Locator, Page } from playwright/test; export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page page; // 使用Playwright推荐的选择器策略优先使用角色(role)或文本(text) this.usernameInput page.getByRole(textbox, { name: /用户名|账号|email/i }); this.passwordInput page.getByRole(textbox, { name: /密码/i }).or(page.getByRole(textbox, { type: password })); this.loginButton page.getByRole(button, { name: /登录|sign in/i }); this.errorMessage page.getByText(/用户名或密码错误|invalid credentials/i); } async goto() { await this.page.goto(/login); // 相对于baseURL } async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.loginButton.click(); } }选择器策略心得对于低代码平台生成的界面元素的ID或类名可能是动态的、不稳定的。因此优先使用getByRole()和getByText()这类基于语义和内容的选择器它们更具可读性和稳定性。getByRole(‘textbox’, { name: /用户名/i })的意思是“找到一个角色是文本框且可访问名称包含‘用户名’的元素”。然后在步骤文件中导入并使用这个页面对象。// steps/login.steps.ts import { Given, When, Then } from ../fixtures/fixtures; import { LoginPage } from ../pages/LoginPage; import { expect } from playwright/test; // 使用Playwright的断言 Given(我打开了ILLA登录页面, async ({ page }) { const loginPage new LoginPage(page); await loginPage.goto(); }); When(我在{string}输入框中输入 {string}, async ({ page }, fieldName: string, value: string) { const loginPage new LoginPage(page); if (fieldName.includes(用户名)) { await loginPage.usernameInput.fill(value); } else if (fieldName.includes(密码)) { await loginPage.passwordInput.fill(value); } else { throw new Error(未知的输入框: ${fieldName}); } }); When(我点击{string}按钮, async ({ page }, buttonName: string) { const loginPage new LoginPage(page); if (buttonName.includes(登录)) { await loginPage.loginButton.click(); } }); Then(我应该被重定向到应用管理面板, async ({ page }) { // 断言URL包含管理面板的路径 await expect(page).toHaveURL(/\/dashboard|\/apps/); // 或者断言页面上出现了管理面板特有的元素 await expect(page.getByText(我的应用)).toBeVisible(); }); Then(我应该能看到用户头像显示 {string}, async ({ page }, username: string) { await expect(page.getByRole(button, { name: username })).toBeVisible(); }); Then(我应该看到错误提示信息 {string}, async ({ page }, message: string) { const loginPage new LoginPage(page); await expect(loginPage.errorMessage).toBeVisible(); await expect(loginPage.errorMessage).toContainText(message); }); Then(我应该仍然停留在登录页面, async ({ page }) { await expect(page).toHaveURL(/\/login/); });步骤定义技巧参数化使用{string}或{int}等占位符可以让同一步骤模式匹配多个场景减少代码重复。使用Playwright断言expect来自playwright/test它内置了自动等待比直接使用Node的assert更可靠。清晰的错误信息在步骤中抛出有意义的错误有助于快速定位是业务逻辑问题还是自动化脚本问题。5. 应对ILLA Builder动态内容的挑战这是Playwright E2E测试实践中最关键、也最容易出问题的部分。正如网络热词中提到的“录制脚本最常见的失败原因就是动态内容”。ILLA Builder生成的界面其组件ID、类名甚至DOM结构都可能随着版本或不同配置而变化。5.1 动态内容定位策略绝对避免使用录制工具生成的脆弱选择器录制工具常生成类似#root div div:nth-child(2) div button的绝对路径选择器一个DOM结构微调就会导致失败。拥抱语义化选择器getByRole()这是首选。为ILLA组件配置清晰的aria-label或确保其有正确的ARIA角色。如果组件本身没有可以考虑在测试环境中通过少量代码注入来临时添加但这属于进阶用法。getByText()/getByLabelText()根据可见文本或标签文本定位。例如定位一个显示“提交”的按钮。getByTestId()这是“黄金法则”。与开发约定为关键的可交互元素添加固定的>// 找到表格中第一行“状态”为“进行中”的行的“操作”按钮 const row page.getByRole(row).filter({ hasText: 进行中 }); const actionBtn row.getByRole(button, { name: 操作 });5.2 等待策略告别硬编码的sleepPlaywright的自动等待已经很强大了但在一些极端异步场景下可能需要更精细的控制。// 反例绝对不要这样做 await page.waitForTimeout(5000); // 硬等待5秒 // 正例使用Playwright的内置等待 // 1. 等待元素出现、可见、可点击 await page.getByTestId(modal).waitFor({ state: visible }); await page.getByRole(button).click(); // click()本身就会等待元素可操作 // 2. 等待网络请求完成对于ILLA的数据查询、API调用非常有用 // 方法一等待特定请求的响应 const responsePromise page.waitForResponse(response response.url().includes(/api/query) response.status() 200 ); await page.getByTestId(refresh-btn).click(); const response await responsePromise; const data await response.json(); // 接下来可以用data做断言 // 方法二等待所有指定类型的请求完成 await Promise.all([ page.waitForLoadState(networkidle), // 等待到网络空闲没有超过500ms的请求 page.getByTestId(load-data-btn).click(), ]); // 3. 等待页面导航完成 await page.getByText(跳转).click(); await page.waitForURL(**/target-page); // 使用通配符匹配URL实战心得对于ILLA中通过数据源查询并渲染表格的场景最稳健的等待方式是结合元素状态和网络请求。例如先点击查询按钮同时等待对应的API响应成功返回然后再去断言表格中是否出现了预期的数据行。6. 复杂场景测试ILLA Builder画布与应用预览测试ILLA Builder本身拖拽组件、配置属性和测试它构建出的应用是两种略有不同的场景。6.1 测试Builder画布交互这涉及到模拟拖拽、点击配置面板等操作。Playwright提供了强大的鼠标和键盘API。// pages/BuilderCanvas.ts - 封装画布操作 import { Page, Locator } from playwright/test; export class BuilderCanvas { readonly page: Page; readonly componentLibrary: Locator; readonly canvasArea: Locator; readonly configPanel: Locator; constructor(page: Page) { this.page page; this.componentLibrary page.getByTestId(component-library); this.canvasArea page.getByTestId(canvas-area); this.configPanel page.getByTestId(config-panel); } async dragComponentToCanvas(componentName: string) { const component this.componentLibrary.getByText(componentName); const box await this.canvasArea.boundingBox(); if (!box) throw new Error(Canvas area not found); await component.hover(); await this.page.mouse.down(); // 将组件拖拽到画布中央 await this.page.mouse.move(box.x box.width / 2, box.y box.height / 2); await this.page.mouse.up(); } async setComponentProperty(propertyName: string, value: string) { // 假设配置面板的属性是标签输入框的形式 const propertyLabel this.configPanel.getByText(propertyName, { exact: true }); const propertyInput propertyLabel.locator(..).getByRole(textbox); // 找到相邻的输入框 await propertyInput.fill(value); } }// 在步骤定义中使用 When(我将一个{string}组件拖拽到画布上, async ({ page }, componentName) { const builder new BuilderCanvas(page); await builder.dragComponentToCanvas(componentName); }); When(我将该组件的{string}属性设置为{string}, async ({ page }, propName, value) { const builder new BuilderCanvas(page); await builder.setComponentProperty(propName, value); });6.2 测试生成的应用预览/发布模式测试构建出的应用更接近于测试一个标准Web应用。关键在于如何导航到已发布的应用URL。一种常见模式是在测试数据中预置一个应用ID或URL。// features/data_display.feature Scenario: 验证表格组件数据绑定 Given 我已发布一个包含表格组件的应用 When 我访问该应用的预览页面 And 表格成功加载了来自 users 数据源的数据 Then 表格中应显示至少10行数据 And 第一行的 姓名 列不应为空// steps/app_viewer.steps.ts import { AppViewerPage } from ../pages/AppViewerPage; import { testData } from ../fixtures/test-data; // 从夹具导入测试数据 Given(我已发布一个包含表格组件的应用, async ({}) { // 这个步骤可能不涉及UI操作而是准备测试数据。 // 例如通过ILLA的API创建一个应用或者使用一个已知的测试应用ID。 // 这里我们假设testData中已经定义好了。 console.log(使用测试应用ID: ${testData.publishedAppId}); }); When(我访问该应用的预览页面, async ({ page }) { const appViewer new AppViewerPage(page); // 拼接出应用的预览URL await appViewer.goto(/apps/${testData.publishedAppId}/preview); }); When(表格成功加载了来自 {string} 数据源的数据, async ({ page }, dataSourceName) { const appViewer new AppViewerPage(page); // 等待表格加载指示器消失并且数据行出现 await appViewer.waitForTableToLoad(dataSourceName); }); Then(表格中应显示至少{int}行数据, async ({ page }, expectedMinRows) { const appViewer new AppViewerPage(page); const rowCount await appViewer.getTableRowCount(); expect(rowCount).toBeGreaterThanOrEqual(expectedMinRows); });7. 高级技巧与CI/CD集成7.1 使用Fixture管理测试状态Playwright Test的Fixture机制非常适合管理测试的全局状态如登录态。我们可以创建一个loggedInPagefixture让所有需要登录的测试场景直接使用一个已登录的页面上下文。// fixtures/logged-in-fixture.ts import { test as base, expect } from playwright/test; import { LoginPage } from ../pages/LoginPage; // 声明Fixture的类型 export type LoggedInFixture { loggedInPage: Page; }; // 扩展基础的test对象 export const test base.extendLoggedInFixture({ loggedInPage: async ({ page, context }, use) { const loginPage new LoginPage(page); await loginPage.goto(); await loginPage.login(admin, process.env.TEST_PASSWORD!); // 密码从环境变量读取 // 可选验证登录成功 await expect(page).toHaveURL(/\/dashboard/); // 将已登录的page传递给测试 await use(page); // 测试结束后可以在这里执行清理操作如退出登录 // await page.getByRole(button, { name: 退出 }).click(); }, }); export { expect };然后在步骤定义或测试文件中就可以直接使用loggedInPage了。// 在步骤定义中使用loggedInPage fixture Given(我是一个已登录的管理员, async ({ loggedInPage }) { // loggedInPage 已经是一个登录后的页面对象可以直接使用 // 这里可能不需要做任何操作或者可以导航到特定页面 await loggedInPage.goto(/apps); });7.2 集成到CI/CD流水线自动化测试的价值在CI/CD中才能最大化体现。以GitHub Actions为例一个基本的配置如下# .github/workflows/playwright-e2e.yml name: Playwright E2E Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps chromium env: PLAYWRIGHT_DOWNLOAD_HOST: https://npmmirror.com/mirrors/playwright # 使用国内镜像加速 - name: Build ILLA Application (if needed) run: | # 这里构建你的ILLA应用假设构建后产物在dist目录 npm run build - name: Run Playwright tests run: npx playwright test env: BASE_URL: ${{ secrets.BASE_URL || http://localhost:3000 }} # 优先使用secret否则用本地 TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} - uses: actions/upload-artifactv4 if: always() # 即使测试失败也上传报告 with: name: playwright-report path: playwright-report/ retention-days: 7 - uses: actions/upload-artifactv4 if: failure() # 仅在失败时上传追踪文件和截图 with: name: playwright-traces path: test-results/CI配置要点使用缓存可以缓存node_modules和Playwright的浏览器安装目录以加速流程。环境变量敏感信息如测试账号密码、BASE_URL务必通过GitHub Secrets管理。报告归档将HTML报告和失败时的trace文件作为产物上传便于在线查看失败详情。并行执行在playwright.config.ts中设置了workersCI环境中可以利用多核并行跑测试大幅缩短反馈时间。8. 常见问题排查与调试技巧实录即使准备得再充分测试失败也在所难免。以下是几个我高频遇到的坑和解决方法。8.1 元素定位失败最常见症状TimeoutError: locator.click: Timeout 30000ms exceeded.排查打开Playwright的headless: false模式亲眼看看测试运行时页面是什么状态。使用playwright codegen工具重新录制定位器但不要直接使用其生成的代码而是观察它使用了哪种选择器策略学习其思路。在测试脚本中临时加入await page.pause();让测试暂停然后打开Playwright Inspector进行交互式调试。使用page.screenshot({ path: ‘debug.png’ })在失败前截图。根本解决回归到语义化选择器和**>// 不可靠 await page.click(‘button’); expect(await page.textContent(‘.status’)).toBe(‘Done’); // 可靠 const responsePromise page.waitForResponse(‘**/api/submit’); await page.click(‘button’); await responsePromise; // 等待提交API完成 await expect(page.locator(‘.status’)).toHaveText(‘Done’); // 断言自带等待8.3 测试在CI上通过本地却失败或反之可能原因环境差异本地开发环境与CI的数据库、API服务数据不同。资源加载速度CI环境网络或服务器可能较慢需要增加超时时间。浏览器版本确保CI和本地安装的Playwright浏览器版本一致。在playwright.config.ts中锁定版本并在CI中使用npx playwright install安装指定版本。解决使用测试数据隔离。每个测试套件或用例都应有独立的数据集并在测试前后进行清理setup/teardown。可以通过API在测试开始前创建测试数据测试结束后删除。在CI配置中适当增加全局超时timeout-minutes和测试操作的超时时间。统一依赖版本使用npx playwright install确保安装的浏览器版本与package.json中playwright/test版本兼容。8.4 利用Trace Viewer进行深度调试当测试失败时Playwright生成的trace.zip文件是终极调试利器。# 运行测试并生成trace在配置中已设置 trace: ‘on-first-retry’ # 或者手动为单个测试开启 npx playwright test --trace on # 打开Trace Viewer查看失败测试的详细记录 npx playwright show-trace test-results/**/*.zipTrace Viewer会展示测试执行过程中的每一个动作、网络请求、控制台日志和DOM快照。你可以像看视频一样逐帧回放精准定位是哪个操作后页面状态出现了异常。9. 测试数据管理与封装实践稳定的测试离不开可控的测试数据。对于ILLA Builder测试数据管理尤为重要因为很多组件如表单、表格都依赖于后端数据源。9.1 测试数据创建与清理策略我倾向于采用“自包含测试”策略每个测试场景负责创建自己需要的数据并在完成后清理避免测试间相互影响。// fixtures/test-data-manager.ts import { request, APIRequestContext } from playwright/test; export class TestDataManager { private apiContext: APIRequestContext; private createdIds: string[] []; // 记录创建的资源ID用于后续清理 constructor(baseURL: string, token: string) { // 初始化一个API请求上下文用于直接调用后端API准备数据 // 注意这需要你的ILLA后端有相应的API } async createTestUser(userData: any): Promisestring { const response await this.apiContext.post(/api/users, { data: userData }); const json await response.json(); const userId json.id; this.createdIds.push(user:${userId}); return userId; } async createTestApp(appConfig: any): Promisestring { const response await this.apiContext.post(/api/apps, { data: appConfig }); const json await response.json(); const appId json.id; this.createdIds.push(app:${appId}); return appId; } async cleanup() { // 逆序清理避免外键约束问题 for (const idRecord of this.createdIds.reverse()) { const [type, id] idRecord.split(:); try { if (type user) { await this.apiContext.delete(/api/users/${id}); } else if (type app) { await this.apiContext.delete(/api/apps/${id}); } } catch (error) { console.warn(清理资源 ${idRecord} 时失败:, error.message); } } this.createdIds []; } } // 在全局Setup和Teardown中使用 // playwright.config.ts 中配置 // globalSetup: ‘./global-setup.ts’, // globalTeardown: ‘./global-teardown.ts’,// global-setup.ts import { TestDataManager } from ./fixtures/test-data-manager; import { test as setup } from playwright/test; setup(初始化测试数据, async () { const manager new TestDataManager(process.env.BASE_API_URL!, process.env.ADMIN_TOKEN!); // 创建一些全局共享的基础数据 const adminUserId await manager.createTestUser({ username: e2e_admin, role: admin }); // 将数据存储到环境变量或文件中供测试用例使用 process.env.E2E_ADMIN_ID adminUserId; // 可以将manager实例挂载到全局但更推荐通过fixture传递 (global as any).__testDataManager manager; }); // global-teardown.ts import { test as teardown } from playwright/test; teardown(清理测试数据, async () { const manager (global as any).__testDataManager; if (manager) { await manager.cleanup(); } });9.2 在测试用例中使用数据夹具通过自定义Fixture将数据管理能力注入到每个测试中。// fixtures/data-fixture.ts import { test as base } from playwright/test; import { TestDataManager } from ./test-data-manager; export const test base.extend{ testData: { userId: string; appId: string }; dataManager: TestDataManager }({ dataManager: async ({}, use) { const manager new TestDataManager(process.env.BASE_API_URL!, process.env.ADMIN_TOKEN!); await use(manager); // 注意这个cleanup会在每个基于此fixture的测试结束后执行清理该测试创建的数据 await manager.cleanup(); }, testData: async ({ dataManager }, use) { // 为每个测试创建独立的数据 const userId await dataManager.createTestUser({ username: test_user_${Date.now()}, role: viewer }); const appId await dataManager.createTestApp({ name: Test App ${Date.now()}, createdBy: userId }); // 将数据传递给测试 await use({ userId, appId }); // 不需要手动清理dataManager fixture的teardown会处理 }, });在步骤定义中你就可以直接使用这些夹具了Given(我有一个新创建的应用, async ({ testData }) { // testData.appId 可以直接使用 console.log(测试将使用应用ID: ${testData.appId}); // 可能不需要UI操作数据已通过API创建好 });这种模式确保了测试的独立性和可重复性是编写可靠E2E测试的基石。