2026-01-18

Playwright与Cucumber集成:行为驱动开发(BDD)实践

一、当E2E测试遇到BDD:我们为何需要这种组合?

最近在重构团队的自动化测试框架时,我们遇到了一个典型问题:业务人员看不懂测试代码,而开发人员写的测试用例又常常偏离业务初衷。这让我开始重新审视测试框架的选择。

传统E2E测试脚本长这样:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left; visibility: visible;">test('login test', async ({ page }) => { await page.goto('/login'); await page.fill('[#username](javascript:;)', 'testuser'); await page.fill('[#password](javascript:;)', 'pass123'); await page.click('button[type="submit"]'); expect(await page.textContent('.welcome')).toBe('Welcome!'); }); </pre>

业务方看到这个代码的反应通常是:“这确实是在测登录,但……这是我们要的登录场景吗?”

而BDD的方式截然不同。同样的测试,用Cucumber描述是这样的:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left; visibility: visible;">功能:用户登录 场景:有效凭证登录 当用户访问登录页面 当用户输入用户名"testuser" 当用户输入密码"pass123" 当用户点击登录按钮 那么用户应该看到欢迎信息 </pre>

看到区别了吗?第二种写法,产品经理、测试工程师、甚至客户都能看懂。这就是BDD(行为驱动开发)的核心价值——用业务语言描述测试。

二、环境搭建:一步一坑的配置过程

2.1 初始化项目

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">mkdir playwright-bdd-demo cd playwright-bdd-demo npm init -y </pre>

2.2 安装依赖(注意版本兼容性)

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">npm install @playwright/test playwright npm install @cucumber/cucumber @cucumber/pretty-formatter npm install @cucumber/cucumber-playwright --save-dev </pre>

这里有个坑我踩过:Cucumber 10.x版本与Playwright的集成方式和9.x不同。我们选择目前更稳定的组合:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">{ "devDependencies": { "@cucumber/cucumber": "^9.0.0", "@playwright/test": "^1.40.0", "@cucumber/pretty-formatter": "^1.0.0" } } </pre>

2.3 配置playwright.config.js

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">const { defineConfig } = require('@playwright/test'); module.exports = defineConfig({ testDir: './features', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [ ['html', { outputFolder: 'test-results' }], ['@cucumber/pretty-formatter', { output: 'test-results/cucumber-report.txt' }] ], use: { baseURL: 'http://localhost:3000', trace: 'on-first-retry', screenshot: 'only-on-failure' }, projects: [ { name: 'chromium', use: { browserName: 'chromium' } } ] }); </pre>

三、从零开始:第一个BDD测试场景

3.1 创建功能文件

features/login.feature中:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">@login @smoke 功能:用户认证系统 背景: 假设系统已启动并运行 并且用户数据库已初始化 场景大纲:用户登录功能 当用户导航到"<page>"页面 并且用户输入用户名"<username>" 并且用户输入密码"<password>" 并且用户点击登录按钮 那么用户应该看到"<expected_result>" 例子:有效登录 | page | username | password | expected_result | | /login | alice | Pass123! | 欢迎页面 | | /login | bob | Test456@ | 欢迎页面 | 例子:无效凭证 | page | username | password | expected_result | | /login | invalid | wrong | 错误提示消息 | </pre>

注意这里的细节:我们使用了场景大纲例子表格,这是Cucumber的强大功能,可以避免场景重复。

3.2 实现步骤定义

features/step-definitions/login.steps.js中:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">const { Given, When, Then } = require('@cucumber/cucumber'); const { expect } = require('@playwright/test'); const { playwright } = require('@cucumber/cucumber-playwright'); let page; let context; // 钩子函数 BeforeAll(asyncfunction () { const browser = await playwright.chromium.launch({ headless: process.env.HEADLESS !== 'false' }); context = await browser.newContext(); }); Before(asyncfunction () { page = await context.newPage(); }); After(asyncfunction () { if (this.scenario.result.status === 'FAILED') { const screenshot = await page.screenshot(); this.attach(screenshot, 'image/png'); } await page.close(); }); AfterAll(asyncfunction () { await context.close(); }); // 步骤定义 Given('系统已启动并运行', asyncfunction () { // 这里可以添加健康检查 console.log('系统检查通过'); }); When('用户导航到{string}页面', asyncfunction (path) { await page.goto(http://localhost:3000${path}); }); When('用户输入用户名{string}', asyncfunction (username) { await page.fill('[data-testid="username"]', username); }); When('用户输入密码{string}', asyncfunction (password) { await page.fill('[data-testid="password"]', password); }); When('用户点击登录按钮', asyncfunction () { await page.click('[data-testid="login-submit"]'); }); Then('用户应该看到{string}', asyncfunction (expectedText) { // 根据场景不同,检查不同的元素 if (expectedText === '欢迎页面') { await expect(page.locator('.welcome-message')) .toBeVisible({ timeout: 5000 }); } elseif (expectedText.includes('错误')) { await expect(page.locator('.error-message')) .toContainText('用户名或密码错误'); } }); </pre>

四、解决实际问题:异步操作与状态共享

4.1 处理异步加载

实际项目中,经常需要等待元素出现:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">Then('页面应在{int}秒内完成加载', async function (timeout) { await page.waitForLoadState('networkidle', { timeout: timeout * 1000 }); // 等待关键元素出现 await page.waitForSelector('[#main](javascript:;)-content', { state: 'visible', timeout: timeout * 1000 }); }); </pre>

4.2 使用World对象共享状态

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">const { setWorldConstructor } = require('@cucumber/cucumber'); class CustomWorld { constructor() { this.page = null; this.testData = {}; this.apiResponse = null; } async initPage() { if (!this.page) { const browser = await playwright.chromium.launch(); const context = await browser.newContext(); this.page = await context.newPage(); } returnthis.page; } } setWorldConstructor(CustomWorld); When('用户获取API数据', asyncfunction () { const response = awaitthis.page.request.get('/api/user/profile'); this.apiResponse = await response.json(); this.testData.userProfile = this.apiResponse; }); </pre>

五、高级技巧:并行执行与报告生成

5.1 配置并行执行

package.json中添加:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">{ "scripts": { "test:bdd": "cucumber-js --parallel 3", "test:bdd:ci": "cucumber-js --parallel 3 --format html:cucumber-report.html" } } </pre>

5.2 生成美观的报告

安装额外报告工具:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">npm install cucumber-html-reporter --save-dev </pre>

创建报告配置cucumber-report-config.js

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">const reporter = require('cucumber-html-reporter'); const options = { theme: 'bootstrap', jsonFile: 'test-results/cucumber_report.json', output: 'test-results/cucumber_report.html', reportSuiteAsScenarios: true, scenarioTimestamp: true, launchReport: true, metadata: { "测试环境": process.env.ENV || "development", "浏览器": "Chrome", "执行时间": newDate().toLocaleString() } }; reporter.generate(options); </pre>

六、实战中的坑与解决方案

坑1:Playwright的异步与Cucumber的同步

问题:Playwright默认是异步的,但Cucumber步骤定义需要正确处理异步。解决:确保所有步骤函数都使用async/await,并且不忘记return promise。

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">// ❌ 错误写法 When('用户点击按钮', function () { page.click('button'); // 没有等待 }); // ✅ 正确写法 When('用户点击按钮', async function () { await page.click('button'); }); </pre>

坑2:测试数据管理

问题:硬编码的测试数据难以维护。解决:使用数据工厂模式:

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">// features/support/data-factory.js class DataFactory { static getTestUser(role = 'user') { const users = { admin: { username: 'admin', password: 'Admin123!' }, user: { username: 'testuser', password: 'Test123!' } }; return users[role]; } } // 在步骤中使用 When('用户以{string}身份登录', asyncfunction (role) { const user = DataFactory.getTestUser(role); await page.fill('[#username](javascript:;)', user.username); await page.fill('[#password](javascript:;)', user.password); }); </pre>

七、集成到CI/CD流水线

7.1 GitHub Actions配置

<pre data-tool="mdnice编辑器" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 10px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px; text-align: left;">name: BDDTests on:[push,pull_request] jobs: playwright-tests: timeout-minutes:60 runs-on:ubuntu-latest steps: -uses:actions/checkout@v3 -uses:actions/setup-node@v3 -name:Installdependencies run:npmci -name:InstallPlaywrightbrowsers run:npxplaywrightinstall--with-depschromium -name:Startapplication run:npmrunstart:test& -name:RunBDDtests run:npmruntest:bdd:ci env: HEADLESS:'true' -name:Uploadtestresults if:always() uses:actions/upload-artifact@v3 with: name:cucumber-report path:test-results/ </pre>

八、写在最后:我们得到了什么?

经过两个月的实践,我们团队发现这套组合带来了明显的变化:

  1. 沟通成本降低:产品文档几乎可以直接复制为测试场景
  2. 测试覆盖更合理:关注用户行为而非实现细节
  3. 反馈速度加快:失败的测试能明确告诉我们是"什么行为"出了问题

当然,任何技术选型都有代价。BDD增加了前期编写场景的时间,但减少了后期的维护成本和沟通成本。对于我们这样业务逻辑复杂、团队角色多样的项目,这笔交易是值得的。

最后的小建议:不要一开始就追求完美的BDD实践。可以从关键业务流程开始,让团队逐渐适应这种"用业务语言思考测试"的方式。毕竟,工具是为人服务的,而不是相反。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容