本规范涵盖单元测试和集成测试的编写要求,旨在指导开发者在项目中有效编写、组织与执行测试用例,确保代码质量和系统稳定性。
- 测试类命名:
{被测试类名}Test - 示例:
ElectricMeterServiceImplTest
@ExtendWith(MockitoExtension.class)
class ElectricMeterServiceImplTest {
// Mock对象声明
@Mock
private ElectricMeterRepository repository;
@Mock
private ElectricMeterMapper mapper;
// 被测试对象
@InjectMocks
private ElectricMeterServiceImpl electricMeterService;
// 测试数据准备
@BeforeEach
void setUp() {
// 初始化测试数据
}
// 测试方法
}- 使用
@Mock注解声明依赖对象 - 使用
@InjectMocks注解声明被测试对象 - 使用
@ExtendWith(MockitoExtension.class)启用Mockito
// 正例:使用具体参数
when(repository.selectById(1L)).thenReturn(mockEntity);
when(mapper.toVo(mockEntity)).thenReturn(mockVo);
// 反例:过度使用any()
when(repository.selectById(any())).thenReturn(mockEntity);- 格式:
test{方法名}_{场景}_{期望结果} - 示例:
testGetDetail_Success()- 成功场景testGetDetail_NotFound()- 未找到场景testAdd_DeviceModelNotFound()- 设备型号不存在场景
- 正常流程测试
- 边界条件测试
- 异常情况测试
- 业务规则验证测试
@BeforeEach
void setUp() {
mockEntity = new ElectricMeterEntity();
mockEntity.setId(1L);
mockEntity.setMeterName("测试电表");
mockVo = new ElectricMeterVo();
mockVo.setId(1L);
mockVo.setMeterName("测试电表");
}// 正例:具体断言
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getMeterName()).isEqualTo("测试电表");
// 反例:模糊断言
assertThat(result).isNotNull();// 验证方法调用
verify(repository).selectById(1L);
verify(mapper).toVo(mockEntity);
// 验证方法未被调用
verify(repository, never()).updateById(any(ElectricMeterEntity.class));@Captor
private ArgumentCaptor<ElectricMeterEntity> entityCaptor;
@Test
void testUpdate_Success() {
// 执行测试
electricMeterService.update(updateBo);
// 捕获参数
verify(repository).updateById(entityCaptor.capture());
ElectricMeterEntity capturedEntity = entityCaptor.getValue();
// 验证捕获的参数
assertThat(capturedEntity.getId()).isEqualTo(1L);
assertThat(capturedEntity.getMeterName()).isEqualTo("更新后的名称");
}- 需要验证传入参数的具体内容时
- 参数是复杂对象且需要验证多个字段时
- 简单参数验证直接使用具体值
- 如果调用时使用了any,可以在verify时使用ArgumentCaptor具体校验
@Test
void testGetDetail_NotFound() {
// 准备数据
when(repository.selectById(999L)).thenReturn(null);
// 执行并验证异常
assertThatThrownBy(() -> electricMeterService.getDetail(999L))
.isInstanceOf(BusinessException.class)
.hasMessage("电表不存在");
}- 业务异常
- 参数校验异常
- 系统异常处理
- 行覆盖率:≥ 95%
- 分支覆盖率:≥ 85%
- 方法覆盖率:≥ 100%
- 所有public方法
- 核心业务逻辑
- 异常处理逻辑
- 边界条件
// 反例1:过度使用any()
when(repository.selectById(any())).thenReturn(mockEntity);
// 反例2:测试方法过长
@Test
void testComplexScenario() {
// 100+ 行代码
}
// 反例3:测试数据硬编码
assertThat(result.getCreateTime()).isEqualTo("2024-01-01 10:00:00");// 正例1:使用具体参数
when(repository.selectById(1L)).thenReturn(mockEntity);
// 正例2:单一职责测试
@Test
void testAdd_Success() {
// 专注测试添加成功场景
}
// 正例3:使用相对时间断言
assertThat(result.getCreateTime()).isAfter(testStartTime);- 测试方法保持简洁,单个方法不超过50行
- 每个测试方法只测试一个场景
- 测试代码要有良好的可读性
- 避免测试之间的相互依赖
- 合理使用@BeforeEach和@BeforeAll
- 避免重复创建大量测试数据
- 使用@MockBean时注意Spring上下文重用
@ExtendWith(MockitoExtension.class)
class ServiceImplTest {
@Mock
private Repository repository;
@Mock
private Mapper mapper;
@InjectMocks
private ServiceImpl service;
@Captor
private ArgumentCaptor<Entity> entityCaptor;
private Entity mockEntity;
private Vo mockVo;
@BeforeEach
void setUp() {
// 初始化测试数据
}
@Test
void testMethod_Success() {
// Given
when(repository.method(param)).thenReturn(result);
// When
Result actual = service.method(param);
// Then
assertThat(actual).isNotNull();
verify(repository).method(param);
}
@Test
void testMethod_Exception() {
// Given
when(repository.method(param)).thenThrow(new RuntimeException());
// When & Then
assertThatThrownBy(() -> service.method(param))
.isInstanceOf(BusinessException.class);
}
}# 例如运行ems-business-device模块的测试用例
export JAVA_HOME=YOUR_JAVA_HOME && mvn clean compile -DskipTests && mvn test -pl ems-business/ems-business-device -am- 验证模块之间的协作与系统行为一致性:接口、服务、仓储、消息、配置中心等。
- 覆盖关键业务流:用户登录与权限、菜单查询、订单创建与查询、订单完成后消息消费、设备模块上下文选择与配置解析等。
- 相对单元测试,集成测试强调真实 Spring 上下文、真实数据存取、少量必要的外部依赖 Mock。
- 使用
@SpringBootTest启动完整应用上下文。 - 使用
@ActiveProfiles("integrationtest")指定测试运行环境(建议在application-integrationtest配置中隔离测试数据源与中间件配置)。 - 使用
@Transactional+@Rollback保持测试数据隔离与幂等,避免污染数据库。 - 推荐使用 AssertJ/ JUnit 断言与 Mockito 交互验证,与单元测试规范保持一致。
示例(来自现有工程用法):
@SpringBootTest
@ActiveProfiles("integrationtest")
@Transactional
@Rollback
@DisplayName("菜单服务集成测试")
class MenuServiceImplIntegrationTest {
@Autowired private MenuService menuService;
@Test
@DisplayName("查询菜单列表 - 成功场景")
void testFindList_Success() {
List<MenuBo> result = menuService.findList(new MenuQueryDto());
assertThat(result).isNotEmpty();
}
}- 使用
@MockitoBean对外部集成或不稳定依赖进行替换(如第三方支付 SDK、系统级配置服务、分布式事务对象等),保持测试可重复性。 - 遵循"只 Mock 边界依赖"原则:核心业务服务与仓储尽量真实,避免过度 Mock 破坏集成语义。
示例(来自现有工程用法):
@SpringBootTest
@ActiveProfiles("integrationtest")
@Transactional
class OrderServiceImplIntegrationTest {
@Autowired private OrderService orderService;
@Autowired private OrderRepository orderRepository;
@MockitoBean private WxMiniProgramPaySdk wxMiniProgramPaySdk;
@MockitoBean private ConfigService configService;
@MockitoBean private ServiceRateService serviceRateService;
@MockitoBean private Transaction transaction;
@Test
void getDetail_ShouldReturnOrderBo_WhenOrderExists() {
OrderEntity entity = buildOrderEntity("IT-ORDER-001")
.setOrderStatus(OrderStatusEnum.NOT_PAY.name())
.setPaymentChannel(PaymentChannelEnum.OFFLINE.name());
orderRepository.insert(entity);
OrderBo result = orderService.getDetail("IT-ORDER-001");
assertThat(result).isNotNull();
assertThat(result.getOrderSn()).isEqualTo("IT-ORDER-001");
}
}DeviceModuleContext根据useRealDevice切换真实设备与 Mock 实现:useRealDevice=true:从sys_config.device_config读取区域配置,按implName与容器中实现类SimpleName(大小写不敏感)匹配返回服务实现。useRealDevice=false:返回以Mock前缀命名的唯一 Mock 实现。
- 集成测试建议:通过属性覆盖验证两种分支逻辑。
示例(属性覆盖真实/Mock 模式):
@SpringBootTest
@ActiveProfiles("integrationtest")
@TestPropertySource(properties = "useRealDevice=false")
class DeviceModuleContextIntegrationTest {
@Autowired private DeviceModuleContext deviceModuleContext;
@Test
@DisplayName("Mock 模式下返回 MockEnergyService")
void testGetService_MockMode() {
EnergyService service = deviceModuleContext.getService(EnergyService.class, 1);
assertThat(service.getClass().getSimpleName().toLowerCase()).startsWith("mock");
}
}- 推荐使用
@AutoConfigureMockMvc+MockMvc进行 Web 层集成测试,不依赖真实网络端口。 - 验证入参校验、鉴权拦截、业务响应结构。
示例:
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("integrationtest")
class ConfigControllerIntegrationTest {
@Autowired private MockMvc mockMvc;
@Test
@DisplayName("获取系统配置 - 成功")
void testGetConfig_Success() throws Exception {
mockMvc.perform(get("/api/config/device"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data").exists());
}
}- 数据准备:
- 使用仓储
insert/update插入最小必要数据,避免复杂依赖。 - 或使用 Profile 对应的初始化 SQL/数据装载(如独立测试库)。
- 使用仓储
- 验证方式:
- 断言返回对象关键字段;
- 通过仓储查询回写结果验证持久化;
- 对 Mock 依赖使用
verify验证交互次数与参数。
示例(仓储数据验证):
OrderDetailTerminationEntity detail = // ... 业务调用后生成
OrderEntity orderEntity = orderRepository.selectOne(
new LambdaQueryWrapper<OrderEntity>().eq(OrderEntity::getOrderSn, detail.getOrderSn()));
assertNotNull(orderEntity);- 类命名:
{被测类名}IntegrationTest;方法命名:methodName_场景_期望。 - 为测试类与方法使用
@DisplayName中文描述。 - 断言具体到字段与业务语义,配合异常场景
assertThrows/assertThatThrownBy。 - 使用
ArgumentCaptor捕获复杂参数进行验证(可选)。
- 业务流拆解:按领域与模块(用户/权限、组织、设备、订单、消息)定义端到端用例。
- 典型覆盖:
- 用户域:登录失败与成功、菜单加载(来源
MenuServiceImplIntegrationTest)。 - 组织域:组织列表查询与分页、模糊查询(来源
OrganizationServiceImplIntegrationTest)。 - 订单域:订单详情查询(正常/删除/异常枚举)、订单完成后的消息消费链路(建议通过直接调用消费端处理器方法 + 仿真消息载荷验证)。
- 设备域:
DeviceModuleContext在不同areaId下的实现选择与configValue解析异常兜底。 - 系统配置域:
device_config更新覆盖与读取异常(空值/未配置区域)。
- 用户域:登录失败与成功、菜单加载(来源
- 输出规范:为每条用例标注业务目标、前置条件、步骤、期望结果、可视化(流程图/时序图可选)。
- 执行命令(Maven):
- 运行全部测试:
mvn -q test - 仅运行集成测试:
mvn -q -Dtest=*IntegrationTest test
- 运行全部测试:
- 失败处理:连续失败不超过 3 次,若仍失败需停止执行并反馈原因与阻断点(与单元测试规范一致)。
- 反模式:
- 过度 Mock 导致集成意义丧失;
- 在一个测试方法中编排过多步骤与断言;
- 缺乏异常分支与边界条件覆盖。
- 最佳实践:
- 使用最小数据驱动测试;
- 关键外部依赖就地 Mock,核心路径真实;
- 断言语义化、验证交互;
- 对设备模块上下文增加真实/Mock 模式双分支用例。
- 断言、命名、Mock、覆盖率等保持一致与继承;
- 集成测试不强求单元覆盖率指标,但需对关键业务流形成闭环验证;
- 建议将复杂断言下沉到单元测试,集成测试关注协作与流程正确性。
注意事项:
- 本规范基于Mockito和JUnit 5框架
- 遵循项目现有的代码风格和命名规范
- 定期review测试代码,确保质量和维护性
- 测试代码同样需要遵循SOLID原则和KISS原则
如需将本文扩展为"业务流矩阵 + 时序图"的可视化版本,可参考相关业务分析师输出规范进行补充与绘图。