Playwright UI模式与Cucumber:构建现代自动化测试的黄金组合
1. 项目概述:为什么是Playwright UI模式与Cucumber?
在自动化测试领域,我们总是在寻找那个“黄金组合”:既要脚本执行得又快又稳,又要让测试用例写得像自然语言一样清晰易懂,最好还能让业务、开发和测试三方坐在一起顺畅沟通。如果你也和我一样,在Selenium、Appium、Puppeteer等工具中反复横跳,为维护一堆脆弱的定位器和复杂的测试数据而头疼,那么今天聊的这个方案——Playwright的UI模式与Cucumber的结合——很可能就是你一直在找的答案。
简单来说,这个方案的核心是:用Playwright来搞定所有浏览器、移动端Web的自动化操作,它速度快、功能强、稳定性远超前辈;同时,用Cucumber的Gherkin语法来编写测试场景,让自动化测试不再是开发人员的“黑话”,而是业务人员也能看懂的“需求说明书”。这不仅仅是两个工具的简单叠加,而是一种将技术实现与业务表达进行清晰分离的工程实践。我团队在多个中大型Web项目中落地这套方案后,最直观的感受是:测试脚本的编写效率提升了,非技术角色参与评审的门槛降低了,而脚本的稳定性和可维护性更是上了一个台阶。接下来,我就为你彻底拆解这套方案的来龙去脉、实操细节以及那些只有踩过坑才知道的宝贵经验。
2. 核心组件深度解析:Playwright UI模式与Cucumber为何是绝配?
在深入动手之前,我们必须先理解手中的“武器”。为什么是它们俩?各自解决了什么痛点?合在一起又产生了怎样的化学反应?
2.1 Playwright UI模式:不止于“无头”的现代浏览器自动化利器
Playwright是微软开源的新一代浏览器自动化库。很多人知道它跑得快、支持多浏览器(Chromium, Firefox, WebKit),但它的“UI模式”才是真正提升开发体验的杀手锏。
UI模式 vs 无头模式:传统的无头模式(Headless)是在后台默默执行,适合CI/CD流水线。而UI模式则会打开一个可观察的浏览器窗口。这不仅仅是“看得见”那么简单,它内置了测试录制器、时间旅行调试、元素选择器拾取等强大工具。你可以像使用IDE一样,单步执行测试,查看每一步的页面状态、网络请求和Console日志。对于调试那些棘手的异步加载、动态元素问题,UI模式的价值无可替代。
超越Selenium的核心优势:
- 自动等待:Playwright对动态内容的处理是革命性的。它内置了智能等待,在执行如点击、输入等操作前,会自动等待元素变得可交互(可见、启用、稳定)。这从根本上避免了因页面加载或动画导致的“ElementNotInteractableException”,而这类问题在Selenium中需要大量显式等待来修补。
- 强大的选择器引擎:除了常规的CSS和XPath,Playwright支持按文本内容定位(
text=)、按元素属性定位([placeholder="Search"]),甚至可以通过has=来定位包含特定子元素的父元素。这让你能写出更健壮、更贴近用户视角的定位器。 - 网络拦截与模拟:你可以轻松地拦截和修改网络请求,这对于测试错误场景、模拟慢速网络或 mock API 响应至关重要,无需修改后端代码。
- 多上下文与多页面:轻松模拟多个浏览器上下文(如不同的用户会话)、标签页或弹出窗口,非常适合测试涉及多用户或第三方登录的场景。
注意:虽然UI模式主要用于开发和调试,但其底层执行引擎与无头模式完全一致。这意味着你在UI模式下调试通过的脚本,可以无缝切换到无头模式在CI中运行,结果是一致的。
2.2 Cucumber:用业务语言编织测试脚本的框架
Cucumber是一个支持行为驱动开发(BDD)的测试框架。它的核心是Gherkin语言,一种近乎自然语言的领域特定语言(DSL)。
Gherkin语法示例:
功能: 用户登录 场景大纲: 使用有效和无效凭据登录 当 我在登录页面 并且 我输入用户名 "<用户名>" 并且 我输入密码 "<密码>" 并且 我点击登录按钮 那么 我应该看到 "<预期结果>" 例子: | 用户名 | 密码 | 预期结果 | | validUser | correctPwd | 主页欢迎信息 | | invalidUser | wrongPwd | 错误提示消息 |Cucumber带来的核心价值:
- 统一沟通语言:产品经理、业务分析师可以用Gherkin编写验收标准(Acceptance Criteria),这些文件(.feature)本身就是可执行的测试规范。开发、测试、业务三方对需求的理解基于同一份“活文档”,极大减少了沟通歧义。
- 测试即文档:生成的测试报告直接展示了用业务语言描述的测试场景和结果,任何人都能看懂,成为系统行为最直观的文档。
- 关注点分离:Gherkin场景只描述“做什么”(What),不涉及“怎么做”(How)。“怎么做”的实现细节被封装在背后的Step Definitions(步骤定义)代码中。当页面UI变化时,通常只需要更新步骤定义的代码,而大量的.feature文件可以保持稳定。
2.3 强强联合的架构优势
将两者结合,我们得到了一个清晰的分层架构:
- 表现层(.feature文件):用Gherkin编写的、可读性极高的测试场景。这是与业务沟通的桥梁。
- 协调层(Step Definitions):Cucumber的步骤定义代码。它解析Gherkin语句,并调用底层的业务操作。
- 操作层(Page Objects / 业务封装类):这里封装了所有与Playwright交互的细节。一个页面或一个组件对应一个类,包含其所有元素定位器和操作方法。
- 驱动层(Playwright):实际驱动浏览器执行所有自动化操作的引擎。
这种架构确保了代码的高内聚、低耦合。业务逻辑变更,改.feature文件;UI交互变更,改操作层的封装类;只要Playwright的API不变,驱动层就无需改动。维护成本被分摊并降到最低。
3. 环境搭建与项目初始化实战
理论讲完,我们动手搭建一个完整的项目。我将以Java技术栈为例(这也是企业级应用中最常见的选择),使用Maven进行依赖管理。
3.1 初始化Maven项目与依赖配置
首先,创建一个标准的Maven项目。你的pom.xml关键依赖如下:
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <cucumber.version>7.15.0</cucumber.version> <playwright.version>1.45.0</playwright.version> <junit.version>4.13.2</junit.version> <!-- Cucumber JUnit4集成仍很稳定 --> </properties> <dependencies> <!-- Cucumber BDD --> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-java</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit</artifactId> <version>${cucumber.version}</version> <scope>test</scope> </dependency> <!-- Playwright for Java --> <dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>${playwright.version}</version> <scope>test</scope> </dependency> <!-- JUnit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <!-- 日志框架,可选但推荐 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.12</version> <scope>test</scope> </dependency> </dependencies>依赖选型解析:
- Cucumber JUnit:这是为了使用JUnit的测试运行器来执行Cucumber场景。虽然JUnit 5更现代,但Cucumber对JUnit 5的支持(cucumber-junit-platform-engine)在配置上稍复杂,对于刚入门,使用成熟的JUnit 4集成更简单可靠。
- Playwright:直接引入官方Java库。Maven会自动下载对应的浏览器驱动(Chromium, Firefox, WebKit),无需像Selenium那样单独管理WebDriver。
- 日志框架:Playwright和Cucumber都会输出大量有用的调试信息,引入一个简单的日志框架(如SLF4J-Simple)可以更好地控制日志级别和输出。
3.2 项目目录结构规划
一个清晰的项目结构是维护性的基石。我推荐如下结构:
src/test/ ├── java/ │ └── com/ │ └── yourcompany/ │ ├── runners/ # 测试运行器 │ │ └── TestRunner.java │ ├── stepdefinitions/ # 步骤定义 │ │ └── LoginSteps.java │ └── pages/ # 页面对象模型 │ └── LoginPage.java └── resources/ ├── features/ # Gherkin特性文件 │ └── login.feature └── config.properties # 配置文件(如URL、凭据)3.3 编写测试运行器(Test Runner)
TestRunner.java是Cucumber测试的入口点,它告诉Cucumber去哪里找特性文件和步骤定义。
package com.yourcompany.runners; import io.cucumber.junit.Cucumber; import io.cucumber.junit.CucumberOptions; import org.junit.runner.RunWith; @RunWith(Cucumber.class) @CucumberOptions( features = "src/test/resources/features", // 特性文件路径 glue = "com.yourcompany.stepdefinitions", // 步骤定义包路径 plugin = { "pretty", // 在控制台输出彩色报告 "html:target/cucumber-reports/cucumber.html", // 生成HTML报告 "json:target/cucumber-reports/cucumber.json" // 生成JSON报告(可用于CI集成) }, monochrome = false, // 控制台输出使用颜色 tags = "@smoke" // 默认只运行带有@smoke标签的场景,可通过命令行覆盖 ) public class TestRunner { }关键配置说明:
plugin:配置报告生成器。html报告便于人工查看,json报告可以被Jenkins、GitLab CI等工具解析,生成更丰富的仪表盘。tags:这是Cucumber非常强大的功能。你可以在.feature文件中给场景打标签(如@smoke、@regression、@wip),然后在这里控制只运行特定标签的测试。在CI中,可以配置快速运行的冒烟测试(@smoke)和全量回归测试(not @wip)。
4. 核心实现:从Gherkin到Playwright操作
现在,我们从最上层的业务描述开始,一步步实现到底层的浏览器操作。
4.1 编写Gherkin特性文件(.feature)
我们在src/test/resources/features/login.feature中创建一个简单的登录场景。
@smoke @login 功能: 用户登录功能 作为网站用户 我希望能够安全登录我的账户 以便访问个性化内容 场景: 使用有效凭据成功登录 当 我导航到登录页面 并且 我输入有效的用户名和密码 并且 我点击登录按钮 那么 我应该被重定向到主页 并且 我应该看到欢迎信息 场景: 使用无效密码登录失败 当 我导航到登录页面 并且 我输入有效的用户名 并且 我输入错误的密码 并且 我点击登录按钮 那么 我应该仍然停留在登录页面 并且 我应该看到密码错误提示信息实操心得:
- 使用中文还是英文?这取决于团队。如果业务方是中文母语,用中文Gherkin能最大化沟通效率。步骤定义的代码可以用英文,因为变量命名更通用。我团队采用“中文Gherkin + 英文代码”的组合,效果很好。
- 场景描述要避免技术细节:不要出现“在ID为
username的输入框输入”。而应该是“我输入用户名”。技术细节属于步骤定义和页面对象。
4.2 实现页面对象模型(Page Object)
这是与Playwright直接交互的一层。我们创建LoginPage.java。
package com.yourcompany.pages; import com.microsoft.playwright.Page; public class LoginPage { private final Page page; // 元素定位器 - 使用Playwright强大的选择器语法 private final String usernameInput = "input[name='username']"; private final String passwordInput = "input[name='password']"; private final String loginButton = "button:has-text('登录')"; private final String errorMessage = ".alert-error"; // 错误提示元素 private final String welcomeMessage = "#welcome-message"; public LoginPage(Page page) { this.page = page; } // 业务操作方法 public void navigateToLoginPage(String baseUrl) { page.navigate(baseUrl + "/login"); // Playwright会自动等待页面加载到‘load’状态 } public void enterUsername(String username) { // fill()方法会自动清空输入框并输入文本,并等待元素可交互 page.fill(usernameInput, username); } public void enterPassword(String password) { page.fill(passwordInput, password); } public void clickLoginButton() { // click()方法会等待元素可点击后再执行点击 page.click(loginButton); } public boolean isErrorMessageDisplayed() { // isVisible()会检查元素是否存在且可见,并返回布尔值 return page.isVisible(errorMessage); } public String getWelcomeMessage() { // textContent()获取元素文本,innerText()也可用,但textContent性能稍好 return page.textContent(welcomeMessage); } public boolean isOnLoginPage() { // 通过URL或页面特定元素判断是否在登录页 return page.url().contains("/login"); } }Playwright选择器最佳实践:
- 优先使用语义化选择器:如
button:has-text('登录')比#login-btn更好,因为前者不依赖开发人员设定的易变的ID。 - 使用
>package com.yourcompany.stepdefinitions; import com.microsoft.playwright.*; import com.yourcompany.pages.LoginPage; import io.cucumber.java.After; import io.cucumber.java.Before; import io.cucumber.java.Scenario; import io.cucumber.java.zh_cn.*; // 注意:使用中文步骤注解 import org.junit.Assert; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import static org.junit.Assert.assertTrue; public class LoginSteps { // Playwright核心对象 private static Playwright playwright; private static Browser browser; private BrowserContext context; private Page page; // 页面对象 private LoginPage loginPage; // 测试数据 private String baseUrl; private String validUsername; private String validPassword; @Before(order = 1) // order=1表示最先执行 public void loadConfig() throws IOException { Properties prop = new Properties(); InputStream input = getClass().getClassLoader().getResourceAsStream("config.properties"); prop.load(input); baseUrl = prop.getProperty("app.base.url"); validUsername = prop.getProperty("user.valid.username"); validPassword = prop.getProperty("user.valid.password"); } @Before(order = 2) // 其次执行,初始化浏览器 public void setUp() { // 采用懒加载单例模式创建Playwright和Browser,避免重复创建开销 if (playwright == null) { playwright = Playwright.create(); // 推荐使用Chromium,稳定且兼容性好。headless=false即启动UI模式。 browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setSlowMo(500)); // slowMo用于放慢操作,便于调试观察 } // 每个场景一个独立的上下文和页面,实现测试隔离 context = browser.newContext(); page = context.newPage(); loginPage = new LoginPage(page); } @当("我导航到登录页面") public void iNavigateToLoginPage() { loginPage.navigateToLoginPage(baseUrl); } @并且("我输入有效的用户名和密码") public void iEnterValidUsernameAndPassword() { loginPage.enterUsername(validUsername); loginPage.enterPassword(validPassword); } @并且("我输入有效的用户名") public void iEnterValidUsername() { loginPage.enterUsername(validUsername); } @并且("我输入错误的密码") public void iEnterWrongPassword() { loginPage.enterPassword("wrongPassword123"); } @并且("我点击登录按钮") public void iClickTheLoginButton() { loginPage.clickLoginButton(); } @那么("我应该被重定向到主页") public void iShouldBeRedirectedToHomePage() { // 等待URL变化,并断言当前URL包含主页路径 page.waitForURL(url -> url.contains("/home") || url.equals(baseUrl + "/")); assertTrue("未正确跳转到主页", page.url().contains("/home") || page.url().equals(baseUrl + "/")); } @那么("我应该看到欢迎信息") public void iShouldSeeWelcomeMessage() { String welcomeMsg = loginPage.getWelcomeMessage(); assertTrue("欢迎信息未显示或内容不符", welcomeMsg != null && welcomeMsg.contains(validUsername)); } @那么("我应该仍然停留在登录页面") public void iShouldRemainOnLoginPage() { assertTrue("未停留在登录页面", loginPage.isOnLoginPage()); } @那么("我应该看到密码错误提示信息") public void iShouldSeePasswordError() { assertTrue("未显示密码错误提示", loginPage.isErrorMessageDisplayed()); } @After public void tearDown(Scenario scenario) { // 如果场景失败,截屏并附加到Cucumber报告 if (scenario.isFailed()) { byte[] screenshot = page.screenshot(new Page.ScreenshotOptions() .setFullPage(true)); // 截取完整页面 scenario.attach(screenshot, "image/png", scenario.getName() + "_failure"); } // 关闭当前场景的上下文和页面 if (context != null) { context.close(); } } // 在所有测试结束后关闭浏览器和Playwright,可以用@AfterAll (JUnit Jupiter) 或静态方法配合@Before(order=0)来实现,此处为简化示例。 public static void closeBrowser() { if (browser != null) { browser.close(); } if (playwright != null) { playwright.close(); } } }步骤定义中的关键技巧:
- 生命周期管理(@Before, @After):
@Before用于初始化(加载配置、启动浏览器),@After用于清理(关闭页面、截图)。确保每个测试场景在独立、干净的浏览器上下文中运行,避免状态污染。 - 断言:使用JUnit的
Assert或AssertJ等库进行断言。断言应清晰描述失败原因。 - 失败截图:在
@After方法中判断场景是否失败,并截屏。这是定位UI测试失败原因的最重要手段。截图会自动嵌入Cucumber的HTML报告中。 - 中文步骤注解:注意导入
io.cucumber.java.zh_cn.*,并使用@当、@并且、@那么等注解来匹配中文Gherkin步骤。步骤方法名可以任意,但注解中的字符串必须与.feature文件中的步骤完全匹配。
4.4 配置与数据管理
创建
src/test/resources/config.properties文件,管理环境配置和测试数据。# 应用环境配置 app.base.url=https://your-test-app.com # 测试用户凭据 user.valid.username=testuser@example.com user.valid.password=SecurePass123! # Playwright 浏览器配置(可通过系统属性覆盖) # browser.type=chromium # browser.headless=false # browser.slow.mo=500为什么用属性文件?将配置外部化,使得同一套测试代码可以在不同环境(开发、测试、预生产)中运行,只需切换属性文件或通过Maven/JVM参数覆盖属性即可。永远不要将敏感凭据硬编码在代码中。
5. 高级技巧与最佳实践
掌握了基础搭建后,下面这些经验能让你团队的自动化测试工程更加健壮和高效。
5.1 使用Playwright Codegen录制生成基础代码
对于快速生成页面交互代码,Playwright的录制功能(Codegen)是无敌的。在UI模式下运行测试时,你可以开启录制器。
操作流程:
- 在步骤定义的
setUp方法中,确保setHeadless(false)。 - 运行测试,Playwright会打开浏览器窗口。
- 在浏览器窗口中,你可以看到“Record”或“Pick Locator”等工具。
- 手动在页面上进行操作(点击、输入),Playwright会自动在控制台或配套的GUI工具中生成对应的Java代码。
- 将这些代码复制到你的页面对象中。
心得:Codegen非常适合快速生成元素定位器和基础操作序列。但不要完全依赖它。生成的代码可能包含不够健壮的定位器(如依赖文本)。你需要根据最佳实践对其进行优化和封装。
5.2 实现数据驱动测试(Data Tables & Scenario Outline)
Cucumber原生支持强大的数据驱动测试,让你的测试场景更简洁、覆盖更全面。
使用Scenario Outline和Examples: 这在前面登录场景中已经展示。它允许你用一张表格来运行同一个场景多次,每次使用不同的数据。
使用Data Tables处理复杂数据: 当需要传递更结构化的数据(如列表、对象)时,可以使用Data Tables。
场景: 批量创建用户 当 我有以下用户列表 | 姓名 | 邮箱 | 角色 | | 张三 | zhangsan@test.com | 管理员 | | 李四 | lisi@test.com | 普通用户 | 那么 这些用户应该被成功创建在步骤定义中,你可以使用Cucumber提供的
DataTable对象来解析:@当("我有以下用户列表") public void iHaveTheFollowingUserList(io.cucumber.datatable.DataTable dataTable) { List<Map<String, String>> users = dataTable.asMaps(String.class, String.class); for (Map<String, String> user : users) { String name = user.get("姓名"); String email = user.get("邮箱"); String role = user.get("角色"); // 调用相应的页面对象方法创建用户 // userPage.createUser(name, email, role); } }5.3 并行测试与CI/CD集成
并行测试: Playwright和Cucumber都支持并行执行,可以大幅缩短测试套件的总运行时间。
- Playwright:可以创建多个独立的
BrowserContext甚至多个Browser实例,每个测试线程一个。 - Cucumber:可以通过Maven插件(如
cucumber-jvm-parallel-plugin)或JUnit Platform来并行运行特性文件。
一个常见的模式是:在CI服务器上,使用无头模式,并利用多个CPU核心并行运行测试。
CI/CD集成(以Jenkins为例):
- 构建步骤:执行
mvn clean test。 - 报告收集:配置Jenkins收集
target/cucumber-reports/cucumber.json文件。 - 报告展示:安装
Cucumber Reports插件,配置该插件解析上一步收集的JSON文件,生成趋势图和各场景状态的可视化报告。 - 失败处理:配置构建后操作,如果测试失败,将HTML报告和失败截图作为构建产物存档,方便查看。
5.4 常见问题排查与调试技巧
即使有了强大的工具,编写稳定的UI自动化测试依然充满挑战。以下是我总结的常见“坑”及解决方法:
问题现象 可能原因 排查与解决思路 元素找不到(TimeoutError) 1. 定位器不正确或已过期。
2. 元素在iframe内。
3. 页面加载/元素渲染过慢。1.使用Playwright Inspector:在UI模式下运行,用 page.pause()暂停测试,打开Inspector检查元素,验证并生成新的定位器。
2.处理iframe:使用page.frame()切换到正确的iframe上下文后再操作。
3.调整等待策略:检查是否使用了page.waitForSelector()或page.waitForFunction()等待动态内容。Playwright的自动等待通常足够,但极端情况需手动干预。操作执行失败(如点击无效) 1. 元素被遮挡(弹窗、遮罩层)。
2. 元素状态不可交互(disabled, hidden)。
3. 发生了意外的导航或弹窗。1.强制点击:作为最后手段,使用 page.click(selector, new Page.ClickOptions().setForce(true))。
2.操作前断言:在操作前用page.isEnabled()或page.isVisible()检查元素状态。
3.处理弹窗:使用page.onDialog()监听并处理alert,confirm,prompt。测试在CI上失败,本地却通过 1. 环境差异(URL、数据、网络)。
2. CI环境资源不足(内存、CPU)。
3. 无头模式下的细微渲染差异。1.环境隔离:确保CI使用独立的、稳定的测试环境和数据。
2.增加稳定性:在CI配置中适当增加全局超时时间,或使用setTimeout。
3.使用容器:在Docker容器中运行测试,确保环境一致性。
4.关键步骤添加重试:对于网络请求等不稳定操作,在步骤定义或页面对象方法中添加简单的重试逻辑。Cucumber步骤未执行(Undefined) 1. 步骤定义中的注解字符串与.feature文件不匹配(包括空格、标点)。
2. 步骤定义类未被扫描到(glue路径错误)。1.仔细核对:复制.feature中的步骤到步骤定义注解中,确保完全一致。
2.检查Runner配置:确认@CucumberOptions(glue=...)路径正确指向你的步骤定义包。测试报告没有截图 1. @After方法未正确获取Scenario对象或截图逻辑未执行。
2. 截图保存路径错误或权限问题。1.确保Scenario参数注入: @After方法签名必须是public void tearDown(Scenario scenario)。
2.检查附件逻辑:确保在scenario.isFailed()为true时才执行截图和attach操作。最重要的调试工具——Playwright Inspector: 在
setUp方法中启动浏览器时,添加.setDevtools(true)选项,或直接使用playwright codegen命令启动独立的录制/调试工具。它可以让你实时查看执行的命令、检查元素、查看网络请求和Console日志,是解决复杂交互问题的瑞士军刀。6. 方案总结与演进思考
走到这里,你已经拥有了一个结合了Playwright强大自动化能力和Cucumber清晰业务表达力的现代化测试框架。回顾一下,这个方案的核心优势在于:用Cucumber的Gherkin拉齐了业务与技术团队的认知,用Playwright的稳定性和丰富功能保障了自动化脚本的执行效率,再用清晰的架构(页面对象、步骤定义)确保了代码的长期可维护性。
在实际项目中落地时,我建议采取渐进式策略:
- 从小处着手:先为一个核心业务流程(如登录-下单)编写自动化测试,跑通整个流程,建立团队信心。
- 建立模式与规范:制定团队的页面对象编写规范、步骤定义命名规范、测试数据管理规范。
- 集成到CI:将这套测试作为持续集成流水线中的一环,每次代码提交都自动运行,及时反馈问题。
- 持续重构:随着产品迭代,定期回顾和重构测试代码,合并重复步骤,优化定位器,保持代码健康度。
最后,技术总是在演进。Playwright社区非常活跃,持续关注其新特性(如组件测试、更强大的API测试支持)。同时,也可以探索将Cucumber与更高级的BDD框架(如Serenity BDD)结合,后者能生成极其详尽的、带有步骤截图和业务层描述的活文档报告,对于大型复杂项目尤其有价值。但无论如何,今天搭建的这个“Playwright + Cucumber”核心组合,已经为你提供了一个坚实、现代且高效的自动化测试起点。
- 生命周期管理(@Before, @After):