E2E 测试 CI 集成实战——Headless 模式、Docker 运行、测试报告
2026/4/30大约 6 分钟
E2E 测试 CI 集成实战——Headless 模式、Docker 运行、测试报告
适读人群:DevOps 工程师 / 需要将 E2E 测试集成到 CI/CD 的开发团队 | 阅读时长:约 16 分钟 | 核心价值:完整的 CI 集成方案,让 E2E 测试在流水线中稳定可靠地运行
那次让整个团队等了 45 分钟的流水线
去年我们把 E2E 测试接入 CI 流水线,第一次跑就出了问题。
流水线一直卡着,45 分钟后超时失败,日志里显示:
[chromium] Launching browser...
Error: Failed to launch chromium because executable doesn't exist at
/home/runner/.cache/ms-playwright/chromium-1091/chrome-linux/chrome原来我们在本地测试用的是已下载好的浏览器,在 CI 服务器上没有执行浏览器安装步骤。
然后又出了第二个问题:安装浏览器的步骤成功了,但测试跑起来一直 crash,日志是:
[chromium] Failed to launch chromium!
...
/home/runner/.cache/ms-playwright/chromium-1091/chrome-linux/chrome:
error while loading shared libraries: libnss3.so: cannot open shared object file原因是 CI 环境是个 minimal Linux 镜像,缺少浏览器运行依赖。
这两个问题花了我一整天才搞定。今天把完整方案写出来,让你少走弯路。
Headless 模式最佳配置
Playwright Headless 配置
// BaseTest.java
@BeforeAll
static void setup() {
playwright = Playwright.create();
boolean isCI = "true".equals(System.getenv("CI"));
boolean headless = Boolean.parseBoolean(
System.getProperty("headless", isCI ? "true" : "false")
);
BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions()
.setHeadless(headless)
.setSlowMo(headless ? 0 : 50); // 本地调试时加慢放效果
// CI 环境加额外参数
if (isCI) {
launchOptions.setArgs(List.of(
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-setuid-sandbox",
"--no-first-run",
"--no-zygote",
"--single-process",
"--disable-extensions"
));
}
String browserName = System.getProperty("browser", "chromium");
browser = switch (browserName) {
case "firefox" -> playwright.firefox().launch(launchOptions);
case "webkit" -> playwright.webkit().launch(launchOptions);
default -> playwright.chromium().launch(launchOptions);
};
}Selenium Headless 配置(Chrome 112+ 新模式)
ChromeOptions options = new ChromeOptions();
boolean isCI = "true".equals(System.getenv("CI"));
if (isCI) {
// Chrome 112+ 推荐使用 --headless=new,渲染更接近有头模式
options.addArguments("--headless=new");
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--disable-gpu");
options.addArguments("--window-size=1920,1080");
options.addArguments("--disable-extensions");
options.addArguments("--disable-plugins");
}
WebDriver driver = new ChromeDriver(options);Docker 方案
将 E2E 测试容器化是最推荐的方式,环境完全可重现,不依赖宿主机配置。
Dockerfile
# 使用 Playwright 官方镜像(已包含所有浏览器依赖)
FROM mcr.microsoft.com/playwright/java:v1.40.0-jammy
WORKDIR /app
# 复制 Maven 配置
COPY pom.xml .
# 先下载依赖(利用 Docker 缓存层)
RUN mvn dependency:go-offline -q
# 复制源代码
COPY src ./src
# 执行测试
CMD ["mvn", "test", "-Dheadless=true", "-B"]docker-compose.yml(本地开发用)
version: '3.8'
services:
e2e-tests:
build:
context: .
dockerfile: Dockerfile.test
environment:
- CI=true
- BASE_URL=http://app:8080
- HEADLESS=true
- BROWSER=chromium
volumes:
- ./test-results:/app/test-results
depends_on:
app:
condition: service_healthy
networks:
- test-network
app:
image: your-app:latest
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 10
networks:
- test-network
networks:
test-network:
driver: bridge运行测试:
# 启动 app + 运行测试
docker-compose up --abort-on-container-exit e2e-tests
# 查看测试结果
ls -la test-results/GitHub Actions 完整配置
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 2 * * *' # 每天凌晨 2 点运行一次完整回归
jobs:
e2e-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false # 一个浏览器失败不影响其他浏览器
matrix:
browser: [chromium, firefox, webkit]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Install Playwright browsers
run: mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install --with-deps ${{ matrix.browser }}"
- name: Start application
run: |
# 启动被测应用(后台运行)
java -jar target/app.jar --spring.profiles.active=test &
# 等待应用启动
timeout 60 bash -c 'until curl -sf http://localhost:8080/actuator/health; do sleep 2; done'
- name: Run E2E tests
run: |
mvn test \
-Dbrowser=${{ matrix.browser }} \
-Dheadless=true \
-Dbase.url=http://localhost:8080 \
-B
env:
CI: true
- name: Upload test results
uses: actions/upload-artifact@v4
if: always() # 即使测试失败也上传
with:
name: e2e-results-${{ matrix.browser }}
path: |
test-results/
target/surefire-reports/
retention-days: 7
- name: Upload Playwright traces
uses: actions/upload-artifact@v4
if: failure() # 只在失败时上传 trace(节省存储空间)
with:
name: playwright-traces-${{ matrix.browser }}
path: test-results/traces/
retention-days: 3
- name: Publish test report
uses: dorny/test-reporter@v1
if: always()
with:
name: E2E Tests (${{ matrix.browser }})
path: target/surefire-reports/*.xml
reporter: java-junitJenkins Pipeline 配置
// Jenkinsfile
pipeline {
agent {
docker {
image 'mcr.microsoft.com/playwright/java:v1.40.0-jammy'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
environment {
CI = 'true'
BASE_URL = 'http://app-service:8080'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn compile -q'
}
}
stage('E2E Tests') {
parallel {
stage('Chromium') {
steps {
sh '''
mvn test \
-Dbrowser=chromium \
-Dheadless=true \
-Dsurefire.reportsDirectory=target/surefire-reports/chromium \
-B
'''
}
post {
always {
junit 'target/surefire-reports/chromium/*.xml'
archiveArtifacts artifacts: 'test-results/traces/**', allowEmptyArchive: true
}
}
}
stage('Firefox') {
steps {
sh '''
mvn test \
-Dbrowser=firefox \
-Dheadless=true \
-Dsurefire.reportsDirectory=target/surefire-reports/firefox \
-B
'''
}
post {
always {
junit 'target/surefire-reports/firefox/*.xml'
}
}
}
}
}
stage('Generate Report') {
steps {
// Allure 报告
allure([
reportBuildPolicy: 'ALWAYS',
results: [[path: 'target/allure-results']]
])
}
}
}
post {
failure {
// 失败时发送通知
slackSend(
color: 'danger',
message: "E2E Tests FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}\n${env.BUILD_URL}"
)
}
}
}测试报告配置
Allure 报告(最推荐)
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>2.24.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.12.0</version>
<configuration>
<reportVersion>2.24.0</reportVersion>
</configuration>
</plugin>
</plugins>
</build>在测试中添加 Allure 注解:
import io.qameta.allure.*;
@Epic("用户认证")
@Feature("登录功能")
public class LoginTest extends BaseTest {
@Test
@Story("正常登录")
@Description("验证有效用户可以成功登录到系统")
@Severity(SeverityLevel.CRITICAL)
void shouldLoginWithValidCredentials() {
Allure.step("打开登录页面", () -> {
page.navigate(BASE_URL + "/login");
});
Allure.step("输入用户名和密码", () -> {
page.getByLabel("邮箱").fill("testuser@test.com");
page.getByLabel("密码").fill("Password123");
});
Allure.step("点击登录按钮", () -> {
page.getByRole(AriaRole.BUTTON,
new Page.GetByRoleOptions().setName("登录")).click();
});
Allure.step("验证成功跳转到首页", () -> {
assertThat(page).hasURL(BASE_URL + "/dashboard");
});
}
@AfterEach
void attachScreenshot(TestInfo testInfo) {
byte[] screenshot = page.screenshot();
Allure.addAttachment("测试截图", "image/png",
new ByteArrayInputStream(screenshot), "png");
}
}生成报告:
mvn allure:report
# 打开 target/site/allure-maven-plugin/index.html踩坑实录
坑一:CI 服务器内存不足导致 OOM
现象: 并行运行多个浏览器测试时,CI 服务器 OOM,进程被 kill。
解法:
# GitHub Actions 指定更大内存的 runner
runs-on: ubuntu-latest-4-cores # 4核8G的runner
# 或者减少并行度
strategy:
max-parallel: 2 # 最多同时跑 2 个浏览器<!-- Maven 限制并行线程数 -->
<configuration>
<forkCount>2</forkCount> <!-- 最多 2 个 JVM 进程 -->
<reuseForks>true</reuseForks>
</configuration>坑二:Docker 容器内 Webkit 无法启动
现象: 在 Docker 容器里,Chromium 和 Firefox 都能启动,Webkit 启动失败。
原因: Webkit 在 Linux 上依赖一些额外的系统库,包括 libwoff2 等字体渲染库。
解法: 确保使用 Playwright 官方镜像,不要基于 minimal 镜像自己装依赖:
# 正确:使用官方镜像
FROM mcr.microsoft.com/playwright/java:v1.40.0-jammy
# 错误:自己基于 ubuntu 安装,可能缺依赖
FROM ubuntu:22.04
RUN apt-get install chromium # 不够,还缺很多依赖或者安装时带 --with-deps:
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI \
-D exec.args="install --with-deps webkit"坑三:测试结果上传后路径错误,无法查看
现象: GitHub Actions artifact 上传成功,但下载解压后 Trace 文件路径不对,无法用 Playwright 打开。
解法: 确保 artifact 上传的路径是相对路径,Trace 文件路径在本地和 CI 上保持一致:
// 使用相对路径生成 Trace 文件
context.tracing().stop(new Tracing.StopOptions()
.setPath(Paths.get("test-results/traces/" + testName + ".zip"))
);
// 上传时路径: test-results/traces/
// 下载后的相对路径不变,Playwright CLI 能正确打开CI 集成的最佳实践总结
- 环境变量驱动配置:通过
CI=true自动切换 headless 模式,不要把headless=true写死在代码里 - 浏览器矩阵测试:至少跑 Chromium + Firefox,发布前加 Webkit
- 只在失败时上传 Trace:节省存储空间,失败时才需要排查
- 健康检查等待:被测应用就绪才开始测试,别傻傻地
sleep 30 fail-fast: false:一个浏览器失败不影响其他浏览器的测试结果
