ContiNew Admin集成测试:端到端测试的完整方案
痛点:为什么你的中后台系统需要端到端测试?
作为开发人员,你是否经常遇到这样的场景:单元测试全部通过,集成测试也显示正常,但用户反馈系统某个功能无法使用?或者某个看似无关的修改导致了意想不到的连锁反应?这就是传统测试方法的局限性——它们无法完全模拟真实用户的使用场景。
ContiNew Admin作为一款高质量的多租户中后台管理系统,其复杂性要求我们采用更全面的测试策略。端到端测试(End-to-End Testing,简称E2E测试)正是解决这一痛点的最佳方案。
读完本文你能得到什么
- 🎯 完整的E2E测试架构设计:从零搭建ContiNew Admin的端到端测试环境
- 🔧 实战测试工具链配置:基于TestContainers、RestAssured、Selenium的完整方案
- 📊 多租户场景测试策略:针对SaaS架构的特殊测试考量
- 🚀 CI/CD集成方案:将E2E测试无缝集成到持续交付流程中
- 💡 最佳实践与避坑指南:基于真实项目经验的实用建议
ContiNew Admin测试架构全景图
环境搭建与依赖配置
Maven依赖配置
在continew-server模块的pom.xml中添加E2E测试依赖:
<dependencies>
<!-- TestContainers 核心 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<!-- MySQL TestContainer -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<!-- Redis TestContainer -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>redis</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
<!-- RestAssured for API testing -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<!-- Selenium WebDriver -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.20.0</version>
<scope>test</scope>
</dependency>
<!-- Selenium TestContainer -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>selenium</artifactId>
<version>1.19.7</version>
<scope>test</scope>
</dependency>
</dependencies>
测试配置文件
创建src/test/resources/application-e2e.yml:
spring:
datasource:
url: jdbc:tc:mysql:8.0.42:///continew_admin?TC_INITSCRIPT=file:src/test/resources/db/test-data.sql
username: test
password: test
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
data:
redis:
host: localhost
port: 6379
password: testpassword
logging:
level:
top.continew.admin: DEBUG
org.testcontainers: INFO
org.springframework.test: DEBUG
server:
port: 18000
# 测试环境特殊配置
continew:
test:
mode: true
base-url: http://localhost:18000
admin-user: admin
admin-password: admin123
核心测试架构实现
1. 基础测试基类设计
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@ActiveProfiles("e2e")
@Slf4j
public abstract class BaseE2ETest {
@Container
public static MySQLContainer<?> mysqlContainer = new MySQLContainer<>("mysql:8.0.42")
.withDatabaseName("continew_admin")
.withUsername("test")
.withPassword("test")
.withInitScript("db/test-data.sql");
@Container
public static GenericContainer<?> redisContainer = new GenericContainer<>("redis:7.2.8")
.withExposedPorts(6379)
.withCommand("redis-server", "--requirepass", "testpassword");
@LocalServerPort
protected int serverPort;
protected String baseUrl;
@BeforeEach
void setUp() {
baseUrl = "http://localhost:" + serverPort;
System.setProperty("spring.datasource.url", mysqlContainer.getJdbcUrl());
System.setProperty("spring.datasource.username", mysqlContainer.getUsername());
System.setProperty("spring.datasource.password", mysqlContainer.getPassword());
System.setProperty("spring.data.redis.host", redisContainer.getHost());
System.setProperty("spring.data.redis.port", String.valueOf(redisContainer.getMappedPort(6379)));
System.setProperty("spring.data.redis.password", "testpassword");
}
protected String getAuthToken() {
return given()
.contentType(ContentType.JSON)
.body("{\"username\":\"admin\",\"password\":\"admin123\"}")
.when()
.post(baseUrl + "/api/auth/login")
.then()
.statusCode(200)
.extract()
.path("data.token");
}
}
2. API层端到端测试
class AuthE2ETest extends BaseE2ETest {
@Test
void shouldLoginSuccessfully() {
// Given
LoginReq loginReq = new LoginReq();
loginReq.setUsername("admin");
loginReq.setPassword("admin123");
loginReq.setAuthType(AuthTypeEnum.ACCOUNT);
// When & Then
given()
.contentType(ContentType.JSON)
.body(loginReq)
.when()
.post(baseUrl + "/api/auth/login")
.then()
.statusCode(200)
.body("success", equalTo(true))
.body("data.token", notNullValue())
.body("data.userInfo.username", equalTo("admin"));
}
@Test
void shouldAccessProtectedResourceWithValidToken() {
// Given
String token = getAuthToken();
// When & Then
given()
.header("Authorization", "Bearer " + token)
.when()
.get(baseUrl + "/api/system/user")
.then()
.statusCode(200)
.body("success", equalTo(true));
}
@Test
void shouldRejectAccessWithInvalidToken() {
given()
.header("Authorization", "Bearer invalid-token")
.when()
.get(baseUrl + "/api/system/user")
.then()
.statusCode(401);
}
}
3. 多租户隔离测试
class MultiTenantE2ETest extends BaseE2ETest {
@Test
void shouldIsolateDataBetweenTenants() {
// 创建两个租户的管理员token
String tenant1Token = createTenantAdminToken("tenant1");
String tenant2Token = createTenantAdminToken("tenant2");
// 租户1创建用户
given()
.header("Authorization", "Bearer " + tenant1Token)
.contentType(ContentType.JSON)
.body("{\"username\":\"user1\",\"password\":\"password123\"}")
.when()
.post(baseUrl + "/api/system/user")
.then()
.statusCode(200);
// 租户2不应该看到租户1的用户
given()
.header("Authorization", "Bearer " + tenant2Token)
.when()
.get(baseUrl + "/api/system/user")
.then()
.statusCode(200)
.body("data.records", hasSize(1)) // 只能看到自己创建的管理员
.body("data.records[0].username", equalTo("admin"));
}
private String createTenantAdminToken(String tenantCode) {
// 简化实现,实际项目中需要先创建租户和对应的管理员
return given()
.contentType(ContentType.JSON)
.body("{\"username\":\"admin\",\"password\":\"admin123\",\"tenantCode\":\"" + tenantCode + "\"}")
.when()
.post(baseUrl + "/api/auth/login")
.then()
.extract()
.path("data.token");
}
}
4. UI层端到端测试
class UserManagementE2ETest extends BaseE2ETest {
@Test
void shouldCompleteUserCRUDFlow() {
WebDriver driver = new ChromeDriver();
try {
// 登录
driver.get(baseUrl + "/login");
driver.findElement(By.name("username")).sendKeys("admin");
driver.findElement(By.name("password")).sendKeys("admin123");
driver.findElement(By.cssSelector("button[type='submit']")).click();
// 等待登录成功
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.urlContains("/dashboard"));
// 导航到用户管理
driver.findElement(By.xpath("//span[text()='系统管理']")).click();
driver.findElement(By.xpath("//span[text()='用户管理']")).click();
// 创建用户
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//button[contains(.,'新增')]"))).click();
driver.findElement(By.name("username")).sendKeys("testuser");
driver.findElement(By.name("password")).sendKeys("Test@123");
driver.findElement(By.name("confirmPassword")).sendKeys("Test@123");
driver.findElement(By.name("email")).sendKeys("test@example.com");
driver.findElement(By.xpath("//button[contains(.,'确定')]")).click();
// 验证用户创建成功
wait.until(ExpectedConditions.visibilityOfElementLocated(
By.xpath("//*[contains(text(),'创建成功')]")));
// 搜索并验证用户存在
driver.findElement(By.cssSelector("input[placeholder='搜索用户名']")).sendKeys("testuser");
driver.findElement(By.cssSelector("button[aria-label='搜索']")).click();
assertTrue(driver.findElement(By.xpath("//*[contains(text(),'testuser')]")).isDisplayed());
} finally {
driver.quit();
}
}
}
测试数据管理策略
数据库初始化脚本
创建src/test/resources/db/test-data.sql:
-- 清理测试数据
DELETE FROM sys_user WHERE username LIKE 'test%';
DELETE FROM sys_tenant WHERE code LIKE 'test%';
-- 插入基础测试数据
INSERT INTO sys_tenant (id, name, code, status, created_time, created_by) VALUES
(1, '默认租户', 'default', 1, NOW(), 1),
(2, '测试租户A', 'test-tenant-a', 1, NOW(), 1),
(3, '测试租户B', 'test-tenant-b', 1, NOW(), 1);
-- 插入管理员用户
INSERT INTO sys_user (id, username, password, nickname, email, status, tenant_id, created_time, created_by) VALUES
(1, 'admin', '$2a$10$r3x4N5s6t7u8v9w0x1y2z3u4v5w6x7y8z9a0b1c2d3e4f5g6h7i8j9k0l1m', '系统管理员', 'admin@continew.top', 1, 1, NOW(), 1),
(2, 'test-admin-a', '$2a$10$r3x4N5s6t7u8v9w0x1y2z3u4v5w6x7y8z9a0b1c2d3e4f5g6h7i8j9k0l1m', '租户A管理员', 'admin-a@test.com', 1, 2, NOW(), 1),
(3, 'test-admin-b', '$2a$10$r3x4N5s6t7u8v9w0x1y2z3u4v5w6x7y8z9a0b1c2d3e4f5g6h7i8j9k0l1m', '租户B管理员', 'admin-b@test.com', 1, 3, NOW(), 1);
-- 插入角色数据
INSERT INTO sys_role (id, name, code, data_scope, status, tenant_id, created_time, created_by) VALUES
(1, '超级管理员', 'super-admin', 1, 1, 1, NOW(), 1),
(2, '租户管理员', 'tenant-admin', 2, 1, 2, NOW(), 2),
(3, '租户管理员', 'tenant-admin', 2, 1, 3, NOW(), 3);
-- 关联用户角色
INSERT INTO sys_user_role (user_id, role_id, created_time, created_by) VALUES
(1, 1, NOW(), 1),
(2, 2, NOW(), 2),
(3, 3, NOW(), 3);
CI/CD集成方案
GitHub Actions工作流配置
创建.github/workflows/e2e-tests.yml:
name: E2E Tests
on:
push:
branches: [ dev, main ]
pull_request:
branches: [ dev, main ]
jobs:
e2e-tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0.42
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: continew_admin
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 3306:3306
redis:
image: redis:7.2.8
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build with Maven
run: mvn clean compile -DskipTests
- name: Run E2E Tests
run: mvn test -Dtest=**/*E2ETest -Dspring.profiles.active=e2e
env:
SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/continew_admin
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root
SPRING_DATA_REDIS_HOST: localhost
SPRING_DATA_REDIS_PORT: 6379
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: e2e-test-results
path: target/surefire-reports/
性能与稳定性考量
测试执行优化策略
| 优化策略 | 实施方法 | 预期效果 |
|---|---|---|
| 测试数据复用 | 使用@BeforeAll初始化基础数据 | 减少70%的数据库操作时间 |
| 并行测试执行 | 配置maven-surefire-plugin并行执行 | 缩短50%的总测试时间 |
| 浏览器复用 | 使用静态WebDriver实例 | 减少浏览器启动开销 |
| 测试数据清理 | 事务回滚或按租户清理 | 避免测试数据污染 |
监控与报告体系
@ExtendWith(TestExecutionListener.class)
class E2ETestMonitor implements TestExecutionListener {
@Override
public void beforeTestExecution(TestContext testContext) {
Metrics.counter("e2e.test.started", "class", testContext.getTestClass().getName())
.increment();
}
@Override
public void afterTestExecution(TestContext testContext) {
String status = testContext.getTestException() == null ? "success" : "failure";
Metrics.counter("e2e.test.completed", "class", testContext.getTestClass().getName(), "status", status)
.increment();
if (testContext.getTestException() != null) {
log.error("E2E Test Failed: {}", testContext.getDisplayName(),
testContext.getTestException());
}
}
}
最佳实践总结
1. 测试金字塔实践
2. 关键成功因素
- 环境一致性:使用TestContainers确保测试环境与生产环境一致
- 数据隔离:每个测试用例使用独立的租户和数据空间
- 执行效率:合理规划测试用例,避免不必要的重复
- 失败分析:完善的日志和报告机制,快速定位问题
3. 常见问题解决方案
| 问题场景 | 解决方案 | 实施要点 |
|---|---|---|
| 测试数据污染 | 使用@DirtiesContext或事务回滚 | 配置@TestExecutionListeners |
| 测试执行慢 | 并行执行+浏览器复用 | 配置maven-surefire-plugin |
| 环境依赖问题 | Docker化测试环境 | 使用TestContainers |
| 前端测试不稳定 | 增加显式等待机制 | 使用WebDriverWait |
结语
通过本文介绍的完整端到端测试方案,ContiNew Admin项目可以获得以下收益:
- 质量保障:确保核心业务功能的正确性和稳定性
- 快速反馈:在CI/CD流水线中快速发现集成问题
- 用户体验:从用户角度验证系统功能的完整性
- 维护效率:减少生产环境的问题排查时间
端到端测试不是银弹,但它是现代软件开发中不可或缺的一环。在ContiNew Admin这样复杂的中后台系统中,合理的E2E测试策略将为项目的长期稳定运行提供坚实保障。
开始行动吧!选择最适合你项目需求的测试场景,逐步构建完善的端到端测试体系,让你的ContiNew Admin项目更加稳健可靠。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



