test: Add comprehensive end-to-end tests for cleaning work order module

This commit is contained in:
lzh
2026-01-22 23:55:05 +08:00
parent e4d07a5306
commit 9750088ca6

View File

@@ -0,0 +1,376 @@
package com.viewsh.module.ops.environment.service.cleanorder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
import com.viewsh.module.ops.core.dispatch.model.DispatchResult;
import com.viewsh.module.ops.core.event.OrderEventPublisher;
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO;
import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper;
import com.viewsh.module.ops.environment.integration.consumer.*;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.module.ops.environment.integration.listener.CleanOrderEventListener;
import com.viewsh.module.ops.environment.service.cleanorder.CleanOrderServiceImpl;
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService;
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import com.viewsh.module.ops.api.queue.OrderQueueService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class CleanOrderEndToEndTest {
// 核心组件
@InjectMocks
private CleanOrderServiceImpl cleanOrderService;
@InjectMocks
private CleanOrderCreateEventHandler createEventHandler;
@InjectMocks
private CleanOrderArriveEventHandler arriveEventHandler;
@InjectMocks
private CleanOrderCompleteEventHandler completeEventHandler;
@InjectMocks
private CleanOrderConfirmEventHandler confirmEventHandler;
@Mock
private CleanOrderEventListener cleanOrderEventListener;
// 依赖 Mocks
@Mock
private OpsOrderMapper opsOrderMapper;
@Mock
private OpsOrderCleanExtMapper cleanExtMapper;
@Mock
private OrderIdGenerator orderIdGenerator;
@Mock
private OrderCodeGenerator orderCodeGenerator;
@Mock
private OrderEventPublisher orderEventPublisher;
@Mock
private OrderQueueService orderQueueService;
@Mock
private DispatchEngine dispatchEngine;
@Mock
private OrderLifecycleManager orderLifecycleManager;
@Mock
private IotDeviceControlApi iotDeviceControlApi;
@Mock
private StringRedisTemplate stringRedisTemplate;
@Mock
private ValueOperations<String, String> valueOperations;
@Mock
private VoiceBroadcastService voiceBroadcastService;
@Mock
private EventLogRecorder eventLogRecorder;
@Spy
private ObjectMapper objectMapper = new ObjectMapper();
// 模拟数据库
private Map<Long, OpsOrderDO> orderDB = new HashMap<>();
private Map<Long, OpsOrderCleanExtDO> cleanExtDB = new HashMap<>();
@BeforeEach
void setUp() {
// 配置 Redis Mock
lenient().when(stringRedisTemplate.opsForValue()).thenReturn(valueOperations);
lenient().when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
// 配置 ID 生成器
lenient().when(orderIdGenerator.generate()).thenAnswer(i -> System.currentTimeMillis());
lenient().when(orderCodeGenerator.generate(anyString())).thenAnswer(i -> "WO" + System.currentTimeMillis());
// 配置 Mapper 模拟 (基础 CRUD)
lenient().when(opsOrderMapper.insert(any(OpsOrderDO.class))).thenAnswer(i -> {
OpsOrderDO order = i.getArgument(0);
if (order.getId() == null) {
order.setId(System.currentTimeMillis());
}
orderDB.put(order.getId(), order);
return 1;
});
lenient().when(opsOrderMapper.selectById(anyLong())).thenAnswer(i -> orderDB.get(i.getArgument(0)));
lenient().when(opsOrderMapper.updateById(any(OpsOrderDO.class))).thenAnswer(i -> {
OpsOrderDO update = i.getArgument(0);
OpsOrderDO existing = orderDB.get(update.getId());
if (existing != null) {
if (update.getStatus() != null) existing.setStatus(update.getStatus());
if (update.getAssigneeId() != null) existing.setAssigneeId(update.getAssigneeId());
if (update.getAssigneeDeviceId() != null) existing.setAssigneeDeviceId(update.getAssigneeDeviceId());
if (update.getAssigneeDeviceKey() != null) existing.setAssigneeDeviceKey(update.getAssigneeDeviceKey());
}
return 1;
});
lenient().when(cleanExtMapper.insert(any(OpsOrderCleanExtDO.class))).thenAnswer(i -> {
OpsOrderCleanExtDO ext = i.getArgument(0);
cleanExtDB.put(ext.getOpsOrderId(), ext);
return 1;
});
// 注入 CleanOrderEventListener
injectField(cleanOrderService, "cleanOrderEventListener", cleanOrderEventListener);
// Stub IotDeviceControlApi for resetTrafficCounter
lenient().when(iotDeviceControlApi.resetTrafficCounter(any()))
.thenReturn(CommonResult.success(true));
}
// ==========================================
// TR-01 ~ TR-03: 工单自动触发与创建测试
// ==========================================
@Test
void testTR01_AutoCreateFromTraffic() throws Exception {
injectField(createEventHandler, "cleanOrderService", cleanOrderService);
injectField(cleanOrderService, "opsOrderMapper", opsOrderMapper);
injectField(cleanOrderService, "cleanExtMapper", cleanExtMapper);
injectField(cleanOrderService, "orderIdGenerator", orderIdGenerator);
injectField(cleanOrderService, "orderCodeGenerator", orderCodeGenerator);
injectField(cleanOrderService, "orderEventPublisher", orderEventPublisher);
// 准备数据
String eventJson = "{" +
"\"eventId\":\"evt-001\"," +
"\"areaId\":101," +
"\"triggerSource\":\"IOT_TRAFFIC\"," +
"\"triggerDeviceId\":8001," +
"\"triggerDeviceKey\":\"traffic-cam-01\"," +
"\"triggerData\":{\"actualCount\":150,\"baseValue\":1000}" +
"}";
// 执行
createEventHandler.onMessage(eventJson);
// 验证
// 1. 验证工单创建
ArgumentCaptor<OpsOrderDO> orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).insert((OpsOrderDO) orderCaptor.capture());
OpsOrderDO order = orderCaptor.getValue();
assertEquals(101L, order.getAreaId());
assertEquals("TRAFFIC", order.getSourceType());
assertEquals("IOT_TRAFFIC", order.getTriggerSource());
// 2. 验证扩展表创建
verify(cleanExtMapper).insert(any(OpsOrderCleanExtDO.class));
// 3. 验证计数器重置调用
verify(iotDeviceControlApi).resetTrafficCounter(any());
}
@Test
void testTR02_Idempotency() throws Exception {
injectField(createEventHandler, "cleanOrderService", cleanOrderService);
// 模拟 Redis 第一次返回 true第二次返回 false
when(valueOperations.setIfAbsent(anyString(), eq("1"), anyLong(), any(TimeUnit.class)))
.thenReturn(true)
.thenReturn(false);
String eventJson = "{\"eventId\":\"evt-duplicate\",\"areaId\":101,\"triggerSource\":\"IOT_TRAFFIC\"}";
// 第一次调用
createEventHandler.onMessage(eventJson);
// 第二次调用
createEventHandler.onMessage(eventJson);
// 验证只创建了一次工单
verify(opsOrderMapper, times(1)).insert(any(OpsOrderDO.class));
}
// ==========================================
// DP-01 ~ DP-03: 智能调度测试 (模拟)
// ==========================================
@Test
void testDP03_P0UrgentInterrupt() {
injectField(cleanOrderService, "dispatchEngine", dispatchEngine);
injectField(cleanOrderService, "opsOrderMapper", opsOrderMapper);
// 准备一个 P0 紧急工单
Long orderId = 999L;
Long cleanerId = 2001L;
OpsOrderDO order = OpsOrderDO.builder()
.id(orderId)
.priority(PriorityEnum.P0.getPriority())
.build();
orderDB.put(orderId, order);
// 模拟调度引擎返回成功
when(dispatchEngine.urgentInterrupt(eq(orderId), eq(cleanerId)))
.thenReturn(DispatchResult.success("P0插队成功", cleanerId));
// 执行派单
cleanOrderService.enqueueAndDispatch(orderId, cleanerId, true);
// 验证
verify(dispatchEngine).urgentInterrupt(eq(orderId), eq(cleanerId));
}
// ==========================================
// AV-01 ~ AV-03: 到岗校验测试
// ==========================================
@Test
void testAV01_BeaconAutoArrive() throws Exception {
injectField(arriveEventHandler, "orderLifecycleManager", orderLifecycleManager);
// 准备工单:状态 DISPATCHED
Long orderId = 1001L;
OpsOrderDO order = OpsOrderDO.builder()
.id(orderId)
.status(WorkOrderStatusEnum.DISPATCHED.getStatus())
.assigneeId(2001L)
.build();
orderDB.put(orderId, order);
String eventJson = "{" +
"\"eventId\":\"evt-arrive-01\"," +
"\"orderId\":1001," +
"\"deviceId\":5001," +
"\"deviceKey\":\"badge-01\"," +
"\"areaId\":101," +
"\"triggerSource\":\"IOT_BEACON\"," +
"\"triggerData\":{\"beaconMac\":\"F0:C8:60:1D:10:BB\"}" +
"}";
// 执行
arriveEventHandler.onMessage(eventJson);
// 验证
// 1. 验证设备ID更新
ArgumentCaptor<OpsOrderDO> orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).updateById((OpsOrderDO) orderCaptor.capture());
OpsOrderDO updatedOrder = orderCaptor.getValue();
assertEquals(orderId, updatedOrder.getId());
assertEquals(5001L, updatedOrder.getAssigneeDeviceId());
// 2. 验证调用了生命周期管理器的 transition
ArgumentCaptor<OrderTransitionRequest> reqCaptor = ArgumentCaptor.forClass(OrderTransitionRequest.class);
verify(orderLifecycleManager).transition(reqCaptor.capture());
OrderTransitionRequest req = reqCaptor.getValue();
assertEquals(orderId, req.getOrderId());
assertEquals(WorkOrderStatusEnum.ARRIVED, req.getTargetStatus());
assertTrue(req.getReason().contains("自动到岗确认"));
// 3. 验证 Redis 缓存更新
verify(valueOperations).set(contains("ops:clean:device:order:5001"), anyString(), anyLong(), any(TimeUnit.class));
}
@Test
void testAV02_StatusInterceptor() throws Exception {
// 准备工单:状态 PENDING (不允许到岗)
Long orderId = 1002L;
OpsOrderDO order = OpsOrderDO.builder()
.id(orderId)
.status(WorkOrderStatusEnum.PENDING.getStatus())
.build();
orderDB.put(orderId, order);
String eventJson = "{\"eventId\":\"evt-arrive-02\",\"orderId\":1002,\"deviceId\":5001}";
// 执行
arriveEventHandler.onMessage(eventJson);
// 验证:不应该调用 transition
verify(orderLifecycleManager, never()).transition(any());
}
// ==========================================
// CP-01 ~ CP-03: 离岗与结单测试
// ==========================================
@Test
void testCP01_SignalLossAutoComplete() throws Exception {
injectField(completeEventHandler, "orderLifecycleManager", orderLifecycleManager);
injectField(completeEventHandler, "cleanOrderService", cleanOrderService);
injectField(cleanOrderService, "dispatchEngine", dispatchEngine);
// 准备工单:状态 ARRIVED
Long orderId = 1003L;
OpsOrderDO order = OpsOrderDO.builder()
.id(orderId)
.status(WorkOrderStatusEnum.ARRIVED.getStatus())
.assigneeId(2001L)
.build();
orderDB.put(orderId, order);
String eventJson = "{" +
"\"eventId\":\"evt-complete-01\"," +
"\"orderId\":1003," +
"\"deviceId\":5001," +
"\"triggerSource\":\"IOT_SIGNAL_LOSS\"," +
"\"triggerData\":{\"durationMs\":1800000}" +
"}";
// 模拟 autoDispatchNext 调用成功
when(dispatchEngine.autoDispatchNext(eq(orderId), eq(2001L)))
.thenReturn(DispatchResult.success("Success", 2001L));
// 执行
completeEventHandler.onMessage(eventJson);
// 验证
// 1. 验证调用了 completeOrder
verify(orderLifecycleManager).completeOrder(eq(orderId), eq(null), contains("信号丢失超时"));
// 2. 验证清理了 Redis 缓存
verify(stringRedisTemplate).delete(contains("ops:clean:device:order:5001"));
// 3. 验证触发了自动调度下一单
verify(dispatchEngine).autoDispatchNext(eq(orderId), eq(2001L));
}
// 简单的反射注入辅助方法
private void injectField(Object target, String fieldName, Object value) {
try {
java.lang.reflect.Field field = getField(target.getClass(), fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
throw new RuntimeException("Failed to inject field: " + fieldName, e);
}
}
private java.lang.reflect.Field getField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
if (clazz.getSuperclass() != null) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
}