ContiNew Admin集成测试:端到端测试的完整方案

ContiNew Admin集成测试:端到端测试的完整方案

【免费下载链接】continew-admin 🔥Almost最佳后端规范🔥页面现代美观,且专注设计与代码细节的高质量多租户中后台管理系统框架。开箱即用,持续迭代优化,持续提供舒适的开发体验。当前采用技术栈:Spring Boot3(Java17)、Vue3 & Arco Design、TS、Vite5 、Sa-Token、MyBatis Plus、Redisson、FastExcel、CosId、JetCache、JustAuth、Crane4j、Spring Doc、Hutool 等。 AI 编程纪元,从 ContiNew & AI 开始优雅编码,让 AI 也“吃点好的”。 【免费下载链接】continew-admin 项目地址: https://gitcode.com/continew/continew-admin

痛点:为什么你的中后台系统需要端到端测试?

作为开发人员,你是否经常遇到这样的场景:单元测试全部通过,集成测试也显示正常,但用户反馈系统某个功能无法使用?或者某个看似无关的修改导致了意想不到的连锁反应?这就是传统测试方法的局限性——它们无法完全模拟真实用户的使用场景。

ContiNew Admin作为一款高质量的多租户中后台管理系统,其复杂性要求我们采用更全面的测试策略。端到端测试(End-to-End Testing,简称E2E测试)正是解决这一痛点的最佳方案。

读完本文你能得到什么

  • 🎯 完整的E2E测试架构设计:从零搭建ContiNew Admin的端到端测试环境
  • 🔧 实战测试工具链配置:基于TestContainers、RestAssured、Selenium的完整方案
  • 📊 多租户场景测试策略:针对SaaS架构的特殊测试考量
  • 🚀 CI/CD集成方案:将E2E测试无缝集成到持续交付流程中
  • 💡 最佳实践与避坑指南:基于真实项目经验的实用建议

ContiNew Admin测试架构全景图

mermaid

环境搭建与依赖配置

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. 测试金字塔实践

mermaid

2. 关键成功因素

  • 环境一致性:使用TestContainers确保测试环境与生产环境一致
  • 数据隔离:每个测试用例使用独立的租户和数据空间
  • 执行效率:合理规划测试用例,避免不必要的重复
  • 失败分析:完善的日志和报告机制,快速定位问题

3. 常见问题解决方案

问题场景解决方案实施要点
测试数据污染使用@DirtiesContext或事务回滚配置@TestExecutionListeners
测试执行慢并行执行+浏览器复用配置maven-surefire-plugin
环境依赖问题Docker化测试环境使用TestContainers
前端测试不稳定增加显式等待机制使用WebDriverWait

结语

通过本文介绍的完整端到端测试方案,ContiNew Admin项目可以获得以下收益:

  1. 质量保障:确保核心业务功能的正确性和稳定性
  2. 快速反馈:在CI/CD流水线中快速发现集成问题
  3. 用户体验:从用户角度验证系统功能的完整性
  4. 维护效率:减少生产环境的问题排查时间

端到端测试不是银弹,但它是现代软件开发中不可或缺的一环。在ContiNew Admin这样复杂的中后台系统中,合理的E2E测试策略将为项目的长期稳定运行提供坚实保障。

开始行动吧!选择最适合你项目需求的测试场景,逐步构建完善的端到端测试体系,让你的ContiNew Admin项目更加稳健可靠。

【免费下载链接】continew-admin 🔥Almost最佳后端规范🔥页面现代美观,且专注设计与代码细节的高质量多租户中后台管理系统框架。开箱即用,持续迭代优化,持续提供舒适的开发体验。当前采用技术栈:Spring Boot3(Java17)、Vue3 & Arco Design、TS、Vite5 、Sa-Token、MyBatis Plus、Redisson、FastExcel、CosId、JetCache、JustAuth、Crane4j、Spring Doc、Hutool 等。 AI 编程纪元,从 ContiNew & AI 开始优雅编码,让 AI 也“吃点好的”。 【免费下载链接】continew-admin 项目地址: https://gitcode.com/continew/continew-admin

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值