静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
✨步子哥 @steper · 2025-11-15 16:17

Frontend-App 测试中 Backend-Service 启动/停止机制调研报告

概述

在 zhichai.graph 项目中,frontend-app 的集成测试需要与 backend-service 进行交互。目前项目采用两种策略来处理测试时的 backend-service 依赖:外部服务依赖内置模拟器。本文档调研了现有的实现方式,并总结相关经验。

当前实现方式

1. 外部服务依赖模式

适用场景:简单集成测试,无需复杂业务逻辑模拟

实现特点

  • 测试假设 backend-service 已经运行
  • 不主动启动/停止服务
  • 依赖外部环境配置
示例文件
  • FrontendBackendIntegrationTest.java
  • LoginLogoutEndToEndTest.java
  • FileUploadUIIntegrationTest.java
  • PagingIntegrationTest.java (当前编辑文件)
代码模式
@SpringBootTest
@ActiveProfiles("test")
class SomeIntegrationTest {
    
    @BeforeEach
    void setUp() {
        // 仅清理Redis数据
        redissonClient.getKeys().flushdb();
    }
    
    @AfterEach
    void tearDown() {
        // 仅清理Redis数据
        redissonClient.getKeys().flushdb();
    }
}

2. 内置模拟器模式

适用场景:复杂业务流程测试,需要精确控制 backend 响应

实现特点

  • 测试内部启动模拟器线程
  • 模拟 backend-service 的命令处理逻辑
  • 测试完成后自动停止模拟器
使用模拟器的测试文件
  • ComplexMultiStepWorkflowTest.java
  • RolePermissionComplexIntegrationTest.java
  • ServerMemberManagementComplexIntegrationTest.java
  • EndToEndBusinessFlowTest.java

模拟器实现详解

核心架构

@SpringBootTest
@ActiveProfiles({"test", "dispatcher-test"})
class ComplexMultiStepWorkflowTest {

    private ExecutorService backendSimulator;
    private volatile boolean running = false;
    
    @BeforeEach
    void setUp() {
        // 初始化ID生成器
        startBackendSimulator();
    }
    
    @AfterEach
    void tearDown() {
        stopBackendSimulator();
    }
}

启动逻辑

private void startBackendSimulator() {
    running = true;
    backendSimulator = Executors.newFixedThreadPool(3);
    
    for (int i = 0; i < 3; i++) {
        backendSimulator.submit(() -> {
            while (running) {
                try {
                    processOneCommand();
                    Thread.sleep(50); // 控制处理频率
                } catch (InterruptedException e) {
                    break;
                } catch (Exception e) {
                    // 忽略错误继续运行
                }
            }
        });
    }
}

命令处理逻辑

private void processOneCommand() throws InterruptedException {
    // 从Redis队列获取命令
    RBlockingQueue<Command> queue = redissonClient.getBlockingQueue(
        RedisKeys.COMMAND_QUEUE_FRONTEND, JsonJacksonCodec.INSTANCE);
    
    Command command = queue.poll(2, TimeUnit.SECONDS);
    if (command == null) {
        return;
    }
    
    // 模拟处理不同类型的命令
    Map<String, Object> data = new HashMap<>();
    Result.Status status = Result.Status.SUCCESS;
    
    switch (command.getCommandType()) {
        case CommandType.USER_REGISTER:
            Long userId = nextId(1L);
            data.put("userId", userId);
            break;
        case CommandType.CREATE_SERVER:
            // 处理服务器创建逻辑
            break;
        // ... 其他命令类型
    }
    
    // 将结果写入Redis Map
    RMap<String, Result> resultMap = redissonClient.getMap(
        RedisKeys.COMMAND_RESULT_MAP, JsonJacksonCodec.INSTANCE);
    resultMap.put(command.getCommandId(), new Result(status, data));
}

停止逻辑

private void stopBackendSimulator() {
    running = false;
    if (backendSimulator != null) {
        backendSimulator.shutdownNow();
        try {
            backendSimulator.awaitTermination(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

经验总结

优势

1. 测试隔离性:模拟器模式下测试完全独立,不依赖外部服务状态 2. 可控性:可以精确控制 backend 的响应行为和时序 3. 性能:模拟器启动快,无需等待真实服务启动 4. 调试友好:模拟器代码可见,便于调试和修改 5. 并发安全:每个测试可以独立运行,不互相干扰

局限性

1. 维护成本:模拟器需要与真实 backend 逻辑保持同步 2. 覆盖不全:模拟器可能无法完全模拟所有 backend 行为 3. 复杂性:增加了测试代码的复杂度 4. 误导风险:模拟器行为与真实服务不一致可能导致测试通过但生产失败

适用场景建议

场景推荐方式理由
简单UI交互测试外部服务依赖减少代码复杂度
复杂业务流程测试内置模拟器需要精确控制流程
错误处理测试内置模拟器便于模拟各种错误场景
性能测试外部服务依赖测试真实性能
端到端测试外部服务依赖测试完整系统

最佳实践

1. 模拟器代码复用:将模拟器逻辑提取为共享工具类 2. 配置化:通过配置文件控制模拟器行为 3. 日志记录:详细记录模拟器处理过程,便于调试 4. 断言验证:不仅验证前端行为,还要验证模拟器收到的命令 5. 定期同步:定期检查模拟器逻辑与真实 backend 是否一致

技术债务

1. 代码重复:4个测试文件都有相似的模拟器实现 2. 维护难度:当 backend 逻辑变化时,需要同时更新多个模拟器 3. 测试覆盖:模拟器可能遗漏某些边界情况

建议改进方案

1. 统一模拟器框架

// 建议创建统一的 BackendSimulator 类
public class BackendSimulator {
    private final ExecutorService executor;
    private final RedissonClient redissonClient;
    private volatile boolean running = false;
    
    public BackendSimulator(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
        this.executor = Executors.newFixedThreadPool(3);
    }
    
    public void start() { /* 启动逻辑 */ }
    public void stop() { /* 停止逻辑 */ }
}

2. 配置驱动的响应

// 通过配置文件定义模拟器行为
@Configuration
public class BackendSimulatorConfig {
    @Bean
    public Map<CommandType, CommandHandler> commandHandlers() {
        // 配置不同命令的处理逻辑
    }
}

3. 混合模式

对于某些测试,可以考虑:

  • 先尝试连接真实 backend
  • 如果连接失败,回退到模拟器模式
  • 通过系统属性控制模式切换

结论

当前项目在处理测试中 backend-service 依赖时采用了务实的方法:简单测试使用外部依赖,复杂测试使用内置模拟器。这种混合方式平衡了测试的实用性和维护性。

建议未来考虑重构模拟器代码,提高复用性,并建立同步机制确保模拟器与真实服务的行为一致。

暂无表态