Files
FrameTour-BE/src/test/java/com/ycwl/basic/puzzle/fill/PuzzleElementFillEngineTest.java
Jerry Yan 27e58d36d0 test(puzzle): 更新测试用例以适配新的执行结果结构
- 移除已弃用的 DeviceCountRangeConditionStrategy 策略注册
- 修改 PuzzleElementFillEngine 执行方法调用方式,使用 getDynamicData 获取动态数据
- 在 PuzzleGenerateServiceImplTest 中引入 FillResult 类型并更新 mock 返回值结构
- 统一调整所有相关测试断言逻辑以匹配新返回的数据格式
2025-11-20 23:10:27 +08:00

447 lines
18 KiB
Java

package com.ycwl.basic.puzzle.fill;
import com.ycwl.basic.mapper.SourceMapper;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleEntity;
import com.ycwl.basic.puzzle.entity.PuzzleFillRuleItemEntity;
import com.ycwl.basic.puzzle.fill.condition.ConditionEvaluator;
import com.ycwl.basic.puzzle.fill.datasource.DataSourceResolver;
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleItemMapper;
import com.ycwl.basic.puzzle.mapper.PuzzleFillRuleMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 拼图元素填充引擎测试
*/
@DisplayName("拼图元素填充引擎测试")
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class PuzzleElementFillEngineTest {
@Mock
private PuzzleFillRuleMapper ruleMapper;
@Mock
private PuzzleFillRuleItemMapper itemMapper;
@Mock
private SourceMapper sourceMapper;
@Mock
private ConditionEvaluator conditionEvaluator;
@Mock
private DataSourceResolver dataSourceResolver;
@InjectMocks
private PuzzleElementFillEngine engine;
@BeforeEach
void setUp() {
// 默认设置
}
@Test
@DisplayName("当没有配置规则时应该返回空Map")
void shouldReturnEmptyMapWhenNoRulesConfigured() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(new ArrayList<>());
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertTrue(result.isEmpty());
verify(ruleMapper, times(1)).listByTemplateId(templateId);
verify(sourceMapper, never()).countDistinctDevicesByFaceId(anyLong());
}
@Test
@DisplayName("应该成功执行匹配的规则并填充多个元素")
void shouldExecuteMatchedRuleAndFillMultipleElements() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
// 模拟规则
PuzzleFillRuleEntity rule = createRule(1L, "4机位规则", 100);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule));
// 模拟机位数量和机位列表
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 模拟条件匹配
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 模拟规则明细
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":0}", "LATEST"),
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
createItem(3L, 1L, "sfp_3", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST"),
createItem(4L, 1L, "sfp_4", "DEVICE_IMAGE", "{\"deviceIndex\":3}", "LATEST")
);
when(itemMapper.listByRuleId(1L)).thenReturn(items);
// 模拟数据源解析
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/img1.jpg")
.thenReturn("https://oss.example.com/img2.jpg")
.thenReturn("https://oss.example.com/img3.jpg")
.thenReturn("https://oss.example.com/img4.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertEquals(4, result.size());
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
assertEquals("https://oss.example.com/img2.jpg", result.get("sfp_2"));
assertEquals("https://oss.example.com/img3.jpg", result.get("sfp_3"));
assertEquals("https://oss.example.com/img4.jpg", result.get("sfp_4"));
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
verify(itemMapper, times(1)).listByRuleId(1L);
verify(dataSourceResolver, times(4)).resolve(anyString(), anyString(), anyString(), anyString(), any());
}
@Test
@DisplayName("缺少faceId时直接返回空结果")
void shouldReturnEmptyWhenRequiredIdsMissing() {
Map<String, String> result = engine.execute(1L, null, 1L).getDynamicData();
assertTrue(result.isEmpty());
verifyNoInteractions(ruleMapper, itemMapper, sourceMapper, conditionEvaluator, dataSourceResolver);
}
@Test
@DisplayName("规则无明细时应继续尝试下一条规则")
void shouldContinueWhenRuleHasNoItems() {
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity highPriorityRule = createRule(1L, "高优先级无明细", 200);
PuzzleFillRuleEntity lowPriorityRule = createRule(2L, "低优先级有效", 100);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(highPriorityRule, lowPriorityRule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(1);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(List.of(99L));
when(conditionEvaluator.evaluate(eq(highPriorityRule), any())).thenReturn(true);
when(conditionEvaluator.evaluate(eq(lowPriorityRule), any())).thenReturn(true);
when(itemMapper.listByRuleId(highPriorityRule.getId())).thenReturn(new ArrayList<>());
PuzzleFillRuleItemEntity lowRuleItem = createItem(10L, lowPriorityRule.getId(), "avatar",
"DEVICE_IMAGE", "{\"deviceIndex\":0}", "LATEST");
when(itemMapper.listByRuleId(lowPriorityRule.getId())).thenReturn(List.of(lowRuleItem));
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/valid.png");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
assertEquals(1, result.size());
assertEquals("https://oss.example.com/valid.png", result.get("avatar"));
verify(conditionEvaluator, times(2)).evaluate(any(), any());
verify(itemMapper, times(2)).listByRuleId(anyLong());
}
@Test
@DisplayName("应该按优先级顺序评估规则并在匹配第一条后停止")
void shouldEvaluateRulesByPriorityAndStopAfterFirstMatch() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
// 模拟3条规则(按priority DESC排序)
PuzzleFillRuleEntity rule1 = createRule(1L, "高优先级规则", 100);
PuzzleFillRuleEntity rule2 = createRule(2L, "中优先级规则", 50);
PuzzleFillRuleEntity rule3 = createRule(3L, "低优先级规则", 10);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule1, rule2, rule3));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 模拟第一条规则不匹配,第二条匹配
when(conditionEvaluator.evaluate(eq(rule1), any())).thenReturn(false);
when(conditionEvaluator.evaluate(eq(rule2), any())).thenReturn(true);
// 模拟规则2的明细
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 2L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST")
);
when(itemMapper.listByRuleId(2L)).thenReturn(items);
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/img.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertEquals(1, result.size());
assertEquals("https://oss.example.com/img.jpg", result.get("sfp_1"));
// 应该评估了rule1和rule2,但没有评估rule3(因为rule2匹配后停止)
verify(conditionEvaluator, times(1)).evaluate(eq(rule1), any());
verify(conditionEvaluator, times(1)).evaluate(eq(rule2), any());
verify(conditionEvaluator, never()).evaluate(eq(rule3), any());
verify(itemMapper, times(1)).listByRuleId(2L);
verify(itemMapper, never()).listByRuleId(1L);
verify(itemMapper, never()).listByRuleId(3L);
}
@Test
@DisplayName("当所有规则都不匹配时应该返回空Map")
void shouldReturnEmptyMapWhenNoRuleMatches() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule1 = createRule(1L, "规则1", 100);
PuzzleFillRuleEntity rule2 = createRule(2L, "规则2", 50);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule1, rule2));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 所有规则都不匹配
when(conditionEvaluator.evaluate(any(), any())).thenReturn(false);
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertTrue(result.isEmpty());
verify(conditionEvaluator, times(2)).evaluate(any(), any());
verify(itemMapper, never()).listByRuleId(anyLong());
}
@Test
@DisplayName("当数据源解析返回null时应该跳过该元素")
void shouldSkipElementWhenDataSourceReturnsNull() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST"),
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{}", "LATEST")
);
when(itemMapper.listByRuleId(1L)).thenReturn(items);
// 第一个返回值,第二个返回null
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/img1.jpg")
.thenReturn(null);
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertEquals(1, result.size());
assertEquals("https://oss.example.com/img1.jpg", result.get("sfp_1"));
assertFalse(result.containsKey("sfp_2"));
}
@Test
@DisplayName("当数据源解析失败时应该使用fallbackValue")
void shouldUseFallbackValueWhenDataSourceFails() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule = createRule(1L, "测试规则", 100);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 明细包含fallbackValue
PuzzleFillRuleItemEntity item = createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{}", "LATEST");
item.setFallbackValue("https://oss.example.com/default.jpg");
when(itemMapper.listByRuleId(1L)).thenReturn(Arrays.asList(item));
// DataSourceResolver内部会处理fallback,这里模拟返回fallback值
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), eq("https://oss.example.com/default.jpg"), any()))
.thenReturn("https://oss.example.com/default.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertEquals(1, result.size());
assertEquals("https://oss.example.com/default.jpg", result.get("sfp_1"));
}
@Test
@DisplayName("当规则匹配但没有明细时应该返回空Map")
void shouldReturnEmptyMapWhenRuleMatchesButHasNoItems() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
PuzzleFillRuleEntity rule = createRule(1L, "空明细规则", 100);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule));
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 规则没有明细
when(itemMapper.listByRuleId(1L)).thenReturn(new ArrayList<>());
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertTrue(result.isEmpty());
}
@Test
@DisplayName("当发生异常时应该返回空Map并记录日志")
void shouldReturnEmptyMapAndLogWhenExceptionOccurs() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
when(ruleMapper.listByTemplateId(templateId))
.thenThrow(new RuntimeException("Database error"));
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertTrue(result.isEmpty());
}
@Test
@DisplayName("应该支持DEVICE_ID_MATCH条件类型并正确填充")
void shouldSupportDeviceIdMatchCondition() {
// Given
Long templateId = 1L;
Long faceId = 123L;
Long scenicId = 1L;
// 模拟规则 - 使用DEVICE_ID_MATCH条件
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(1L);
rule.setRuleName("指定机位规则");
rule.setPriority(100);
rule.setConditionType("DEVICE_ID_MATCH");
rule.setConditionValue("{\"deviceIds\": [200, 300], \"matchMode\": \"ALL\"}");
rule.setEnabled(1);
when(ruleMapper.listByTemplateId(templateId))
.thenReturn(Arrays.asList(rule));
// 模拟机位数量和机位列表
when(sourceMapper.countDistinctDevicesByFaceId(faceId)).thenReturn(4);
when(sourceMapper.getDeviceIdsByFaceId(faceId)).thenReturn(Arrays.asList(100L, 200L, 300L, 400L));
// 模拟条件匹配
when(conditionEvaluator.evaluate(eq(rule), any())).thenReturn(true);
// 模拟规则明细
List<PuzzleFillRuleItemEntity> items = Arrays.asList(
createItem(1L, 1L, "sfp_1", "DEVICE_IMAGE", "{\"deviceIndex\":1}", "LATEST"),
createItem(2L, 1L, "sfp_2", "DEVICE_IMAGE", "{\"deviceIndex\":2}", "LATEST")
);
when(itemMapper.listByRuleId(1L)).thenReturn(items);
// 模拟数据源解析
when(dataSourceResolver.resolve(anyString(), anyString(), anyString(), anyString(), any()))
.thenReturn("https://oss.example.com/device200.jpg")
.thenReturn("https://oss.example.com/device300.jpg");
// When
Map<String, String> result = engine.execute(templateId, faceId, scenicId).getDynamicData();
// Then
assertEquals(2, result.size());
assertEquals("https://oss.example.com/device200.jpg", result.get("sfp_1"));
assertEquals("https://oss.example.com/device300.jpg", result.get("sfp_2"));
// 验证deviceIds被传递到ConditionContext
verify(conditionEvaluator, times(1)).evaluate(eq(rule), any());
verify(sourceMapper, times(1)).getDeviceIdsByFaceId(faceId);
}
// 辅助方法
private PuzzleFillRuleEntity createRule(Long id, String name, Integer priority) {
PuzzleFillRuleEntity rule = new PuzzleFillRuleEntity();
rule.setId(id);
rule.setRuleName(name);
rule.setPriority(priority);
rule.setConditionType("DEVICE_COUNT");
rule.setConditionValue("{\"deviceCount\": 4}");
rule.setEnabled(1);
return rule;
}
private PuzzleFillRuleItemEntity createItem(Long id, Long ruleId, String elementKey,
String dataSource, String sourceFilter, String sortStrategy) {
PuzzleFillRuleItemEntity item = new PuzzleFillRuleItemEntity();
item.setId(id);
item.setRuleId(ruleId);
item.setElementKey(elementKey);
item.setDataSource(dataSource);
item.setSourceFilter(sourceFilter);
item.setSortStrategy(sortStrategy);
item.setItemOrder(id.intValue());
return item;
}
}