何时使用
适用:
- 从零搭建 Playwright Java 项目脚手架(Java 17+ / Playwright 1.44+)。
- 编写页面对象(Page Object)类或 JUnit 5 用例类。
- 跨浏览器测试、并行执行、Allure 报告、trace/视频取证。
- 修复 flaky 测试:把
Thread.sleep()换成显式等待。 - 在 CI/CD(GitHub Actions、Jenkins、Docker)中接入 Playwright。
- 单个用例内混合 API 调用与 UI 断言(hybrid 测试)。
- 用户提到「POM」「BrowserContext」「Playwright fixtures」「traces」。
不该用(负边界):
- 纯 API 测试套件、无任何 UI 交互 —— 用 REST Assured。
- 普通单元测试 / 组件测试 —— 用 JUnit + Mockito,无需启动浏览器。
- 需求、权限、成功标准不清时,先停下来澄清,别硬写。
步骤 / 指令
- 先选方案,再写代码。按下表对号入座:
| 用户诉求 | 方案 |
|---|---|
| 从零新项目 | 完整脚手架(pom.xml + 目录结构) |
| 单个功能测试 | 一个 POM 页面类 + 一个 JUnit5 用例类 |
| API+UI 混合 | APIRequestContext 与 Page 并用 |
| 跨浏览器 | @MethodSource 参数化浏览器名 |
| 修 flaky | sleep 换成 waitFor / waitForResponse |
| CI 接入 | 流水线里跑 install --with-deps |
| 并行执行 | junit-platform.properties + ThreadLocal |
| 富报告 | Allure + trace + 录屏 |
- 固定目录结构(新项目一律照此布局):
src/test/java/com/company/tests/
├── base/ BaseTest.java, BasePage.java
├── pages/ LoginPage.java
├── tests/ LoginTest.java
├── utils/ TestDataFactory.java, WaitUtils.java
└── config/ ConfigReader.java
src/test/resources/
├── test.properties
├── junit-platform.properties
└── testdata/users.json
pom.xml
- 搭线程安全的
BaseTest:用ThreadLocal持有Playwright → Browser → BrowserContext → Page整条链;@BeforeEach建链 + 开 tracing,@AfterEach停 tracing 落盘后逐层关闭。浏览器由-Dbrowser系统属性切换:
public class BaseTest {
protected static ThreadLocal<Playwright> playwrightTL = new ThreadLocal<>();
protected static ThreadLocal<Browser> browserTL = new ThreadLocal<>();
protected static ThreadLocal<BrowserContext> contextTL = new ThreadLocal<>();
protected static ThreadLocal<Page> pageTL = new ThreadLocal<>();
protected Page page() { return pageTL.get(); }
@BeforeEach
void setUp() {
Playwright pw = Playwright.create();
playwrightTL.set(pw);
Browser browser = resolveBrowser(pw).launch(
new BrowserType.LaunchOptions().setHeadless(ConfigReader.isHeadless()));
browserTL.set(browser);
BrowserContext ctx = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(1920, 1080)
.setRecordVideoDir(Paths.get("target/videos/"))
.setLocale("en-US"));
ctx.tracing().start(new Tracing.StartOptions().setScreenshots(true).setSnapshots(true));
contextTL.set(ctx);
pageTL.set(ctx.newPage());
}
@AfterEach
void tearDown(TestInfo info) {
String name = info.getDisplayName().replaceAll("[^a-zA-Z0-9]", "_");
contextTL.get().tracing().stop(new Tracing.StopOptions()
.setPath(Paths.get("target/traces/" + name + ".zip")));
pageTL.get().close(); contextTL.get().close();
browserTL.get().close(); playwrightTL.get().close();
}
private BrowserType resolveBrowser(Playwright pw) {
return switch (System.getProperty("browser", "chromium").toLowerCase()) {
case "firefox" -> pw.firefox();
case "webkit" -> pw.webkit();
default -> pw.chromium();
};
}
}
- 写页面对象类:所有
Locator声明为字段(不在动作方法里内联),优先getByRole / getByLabel / getByTestId;导航方法返回下一个页面对象以支持链式调用:
public class LoginPage extends BasePage {
private final Locator emailInput, passwordInput, loginButton, errorMessage;
public LoginPage(Page page) {
super(page);
emailInput = page.getByLabel("Email address");
passwordInput = page.getByLabel("Password");
loginButton = page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("Sign in"));
errorMessage = page.getByTestId("login-error");
}
@Override protected String getUrl() { return "/login"; }
public DashboardPage loginAs(String email, String password) {
fill(emailInput, email); fill(passwordInput, password);
clickAndWaitForNav(loginButton);
return new DashboardPage(page);
}
public LoginPage loginExpectingError(String email, String password) {
fill(emailInput, email); fill(passwordInput, password);
loginButton.click(); errorMessage.waitFor();
return this;
}
public String getErrorMessage() { return errorMessage.textContent(); }
}
- 写带 Allure 注解的用例:
@ExtendWith(AllureJunit5.class),用@Severity / @DisplayName标注,多字段校验用SoftAssertions,多场景用@ParameterizedTest + @MethodSource:
@ExtendWith(AllureJunit5.class)
class LoginTest extends BaseTest {
private LoginPage loginPage;
@BeforeEach void open() { loginPage = new LoginPage(page()); loginPage.navigate(); }
@Test @Severity(SeverityLevel.BLOCKER)
@DisplayName("有效凭据应跳转到 dashboard")
void shouldLoginWithValidCredentials() {
User u = TestDataFactory.getDefaultUser();
DashboardPage dash = loginPage.loginAs(u.email(), u.password());
assertThat(page()).hasURL(Pattern.compile(".*/dashboard"));
assertThat(dash.getWelcomeBanner()).containsText("Welcome, " + u.firstName());
}
}
- 并行执行配置
src/test/resources/junit-platform.properties:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=fixed
junit.jupiter.execution.parallel.config.fixed.parallelism=4
示例
示例一 · API+UI 混合(用 API 准备数据,比走 UI 快):
APIRequestContext api = page().context().request();
APIResponse resp = api.post("/api/orders", RequestOptions.create()
.setHeader("Authorization", "Bearer " + authToken)
.setData(Map.of("productId", "SKU-001", "quantity", 2)));
assertThat(resp).isOK();
String orderId = new JsonParser().parse(resp.text())
.getAsJsonObject().get("id").getAsString();
OrdersPage orders = new OrdersPage(page()); orders.navigate();
assertThat(orders.getOrderRowById(orderId)).isVisible();
示例二 · 网络打桩(mock 后端故障):
page().route("**/api/products", route -> route.fulfill(new Route.FulfillOptions()
.setStatus(503).setBody("{\"error\":\"Service Unavailable\"}")
.setContentType("application/json")));
new ProductsPage(page()).navigate();
assertThat(products.getErrorBanner())
.hasText("We're having trouble loading products. Please try again.");
示例三 · 跨浏览器并行:
@ParameterizedTest @MethodSource("browsers")
void shouldRenderCheckoutOnAllBrowsers(String browserName) {
System.setProperty("browser", browserName);
new CheckoutPage(page()).navigate();
assertThat(page().locator(".checkout-form")).isVisible();
}
static Stream<String> browsers() { return Stream.of("chromium", "firefox", "webkit"); }
示例四 · GitHub Actions 流水线关键步骤:
- name: Install Playwright browsers
run: mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="install --with-deps"
- name: Run tests
run: mvn test -Dbrowser=${{ matrix.browser }} -Dheadless=true
- name: Upload traces on failure
uses: actions/upload-artifact@v4
if: failure()
with: { name: playwright-traces, path: target/traces/ }
- name: Upload Allure results
uses: actions/upload-artifact@v4
if: always()
with: { name: allure-results, path: target/allure-results/ }
注意事项
要做(✅):
- 并行套件每个用例都用
ThreadLocal<Page>,绝不跨线程共享Page。 Locator字段全部声明在页面类顶部。- 导航方法返回下一个页面对象(链式调用)。
- 用
assertThat(locator)—— 它会自动重试直到超时。 - 优先
getByRole / getByLabel / getByTestId定位。 - tracing 在
@BeforeEach开、@AfterEach停并落盘(不是@AfterAll)。 - 同页多字段校验用
SoftAssertions。 - 跨类复用登录态:保存
storageState跳过重复登录。
不要做(❌):
- 不用
Thread.sleep(),换waitFor()/waitForResponse()。 - 不硬编码 baseURL,统一
ConfigReader.getBaseUrl()。 - 不在页面对象内部
Playwright.create()。 - 动态/频繁变更的元素不用 XPath。
常见坑:
- 并行随机失败 → 检查是否每个用例独立建
Playwright→Browser→Context→Page链。 isVisible()超时 →.setTimeout(10_000)或在BaseTest抬context.setDefaultTimeout()。- 加了
sleep仍 flaky → 换waitForResponse("**/api/endpoint", () -> action())或断言自动轮询。 - trace zip 为空 → 确认
start()在动作前、stop()在@AfterEach。 - Allure 报告空白/缺步骤 → 在
maven-surefire-plugin的<argLine>里加 AspectJ agent。 storageState过期跳登录页 → 重跑AuthSetup重生成target/auth/user-state.json。
互见
- REST Assured(Java)—— 纯 API 测试套件,无 UI 交互时用。
- Selenium(Java)—— 旧方案;新项目一律优先 Playwright。
- Allure 报告 —— 注解、分类、历史趋势深入。
- Testcontainers(Java)—— 测试需要真实数据库/服务时配合本技能。
- GitHub Actions CI —— 构建多浏览器矩阵流水线。
采编自 sickn33/antigravity-awesome-skills(MIT 许可),原作者 amalsam18,已按本仓库 SCHEMA 适配重写。