IndicatorTrendServiceTest.java 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. package com.etotem.cfc.service;
  2. import com.etotem.cfc.entity.IndicatorDefinition;
  3. import com.etotem.cfc.entity.IndicatorValue;
  4. import com.etotem.cfc.mapper.IndicatorDefinitionMapper;
  5. import com.etotem.cfc.mapper.IndicatorValueMapper;
  6. import org.junit.jupiter.api.BeforeEach;
  7. import org.junit.jupiter.api.Test;
  8. import org.springframework.test.util.ReflectionTestUtils;
  9. import java.math.BigDecimal;
  10. import java.sql.Date;
  11. import java.util.ArrayList;
  12. import java.util.Arrays;
  13. import java.util.List;
  14. import java.util.Map;
  15. import static org.junit.jupiter.api.Assertions.*;
  16. import static org.mockito.ArgumentMatchers.any;
  17. import static org.mockito.Mockito.*;
  18. class IndicatorTrendServiceTest {
  19. private IndicatorTrendService service;
  20. private IndicatorValueMapper valueMapper;
  21. private IndicatorDefinitionMapper definitionMapper;
  22. private IndicatorDefinitionMapper changeDefinitionMapper;
  23. private IndicatorTimelineService timelineService;
  24. @BeforeEach
  25. void setUp() {
  26. service = new IndicatorTrendService();
  27. valueMapper = mock(IndicatorValueMapper.class);
  28. ReflectionTestUtils.setField(service, "valueMapper", valueMapper);
  29. definitionMapper = mock(IndicatorDefinitionMapper.class);
  30. ReflectionTestUtils.setField(service, "definitionMapper", definitionMapper);
  31. // 真实 IndicatorChangeService(Q17 后改走 changeService.loadActiveDefinitions),
  32. // 注入 mock mapper 保证单测不碰库
  33. IndicatorChangeService changeService = new IndicatorChangeService();
  34. changeDefinitionMapper = mock(IndicatorDefinitionMapper.class);
  35. ReflectionTestUtils.setField(changeService, "definitionMapper", changeDefinitionMapper);
  36. ReflectionTestUtils.setField(changeService, "valueMapper", valueMapper);
  37. IndicatorDefinition def = new IndicatorDefinition();
  38. def.setId(1L);
  39. def.setName("双歧杆菌属");
  40. def.setUnit("%");
  41. def.setCategory("肠道菌群");
  42. def.setGoodDirection("higher_is_better");
  43. def.setValueType("numeric");
  44. when(changeDefinitionMapper.selectList(any())).thenReturn(new ArrayList<>(Arrays.asList(def)));
  45. ReflectionTestUtils.setField(service, "changeService", changeService);
  46. timelineService = mock(IndicatorTimelineService.class);
  47. ReflectionTestUtils.setField(service, "timelineService", timelineService);
  48. }
  49. private IndicatorValue value(Long defId, String date, String val) {
  50. IndicatorValue v = new IndicatorValue();
  51. v.setDefinitionId(defId);
  52. v.setReportDate(Date.valueOf(date));
  53. v.setValue(val);
  54. v.setNumericValue(new BigDecimal(val));
  55. v.setUnit("%");
  56. v.setReportId(100L);
  57. return v;
  58. }
  59. /** report_date 为 null 的观测(用于验收标准 #4:null 日期不得被当成最新值) */
  60. private IndicatorValue valueWithNullDate(Long defId, String val) {
  61. IndicatorValue v = new IndicatorValue();
  62. v.setDefinitionId(defId);
  63. v.setReportDate(null);
  64. v.setValue(val);
  65. v.setNumericValue(new BigDecimal(val));
  66. v.setUnit("%");
  67. v.setReportId(101L);
  68. return v;
  69. }
  70. private IndicatorDefinition def(Long id, String name, String goodDirection, String unit) {
  71. IndicatorDefinition d = new IndicatorDefinition();
  72. d.setId(id);
  73. d.setName(name);
  74. d.setUnit(unit);
  75. d.setCategory("肠道菌群");
  76. d.setGoodDirection(goodDirection);
  77. d.setValueType("numeric");
  78. d.setStatus(1);
  79. return d;
  80. }
  81. @Test
  82. void trendDelegatesToTimelineService() {
  83. Map<String, Object> expected = new java.util.LinkedHashMap<>();
  84. expected.put("items", new ArrayList<>());
  85. when(timelineService.timeline(any(), any(), any(), any(), any(), any(), any()))
  86. .thenReturn(expected);
  87. Map<String, Object> data = service.trend(3L, Arrays.asList(1L), 2L, null, null, null, "raw", "all");
  88. assertSame(expected, data);
  89. verify(timelineService).timeline(eq(3L), eq(Arrays.asList(1L)), eq(null), eq(null), eq(null), eq("raw"), eq("all"));
  90. }
  91. @Test
  92. void changeRankingAdjacentComputesDeltaAndTrendLabel() {
  93. // 双歧杆菌属 higher_is_better:升幅 → trendLabel=改善
  94. List<IndicatorValue> rows = new ArrayList<>();
  95. rows.add(value(1L, "2026-08-15", "10"));
  96. rows.add(value(1L, "2026-10-01", "12.8"));
  97. when(valueMapper.selectList(any())).thenReturn(rows);
  98. java.util.Map<String, Object> data = service.changeRanking(3L, null, null, "adjacent", "magnitude", 50, 2L,
  99. null, null, null, null, null);
  100. List<?> items = (List<?>) data.get("items");
  101. assertEquals(1, items.size());
  102. Map<?, ?> item = (Map<?, ?>) items.get(0);
  103. assertEquals("up", item.get("movement"));
  104. assertEquals("改善", item.get("trendLabel"));
  105. assertEquals("improved", item.get("trendCode"));
  106. Map<?, ?> overall = (Map<?, ?>) data.get("overall");
  107. assertEquals(1, ((Number) overall.get("improvedCount")).intValue());
  108. }
  109. @Test
  110. void quickLookupReturnsItemsAndCategories() {
  111. List<IndicatorValue> rows = new ArrayList<>();
  112. rows.add(value(1L, "2026-10-01", "12.8"));
  113. when(valueMapper.selectList(any())).thenReturn(rows);
  114. Map<String, Object> data = service.quickLookup(3L, null, 1, 30, 2L, false, 8);
  115. assertTrue(data.containsKey("items"));
  116. assertTrue(data.containsKey("categories"));
  117. List<?> items = (List<?>) data.get("items");
  118. assertEquals(1, items.size());
  119. Map<?, ?> item = (Map<?, ?>) items.get(0);
  120. // reportDate=null 的观测被排在最后(修 bug)
  121. assertEquals("2026-10-01", item.get("latestReportDate"));
  122. assertNotNull(item.get("change"));
  123. }
  124. /** 验收标准 #4:quick-lookup 中 report_date=null 的观测不再被当成最新值 */
  125. @Test
  126. void quickLookup_nullReportDateNotTreatedAsLatest() {
  127. List<IndicatorValue> rows = new ArrayList<>();
  128. rows.add(valueWithNullDate(1L, "999")); // null 日期 → 不参与最新值竞争
  129. rows.add(value(1L, "2026-10-01", "12.8")); // 有日期 → 最新
  130. // 第一次 selectList 拉全部观测;resolvePrevious 的第二次 selectList 返回空(→ first_record)
  131. when(valueMapper.selectList(any())).thenReturn(rows, new ArrayList<>());
  132. Map<String, Object> data = service.quickLookup(3L, null, 1, 30, 2L, false, 8);
  133. Map<?, ?> item = (Map<?, ?>) ((List<?>) data.get("items")).get(0);
  134. assertEquals("2026-10-01", item.get("latestReportDate"), "null 日期观测不得排到最新");
  135. assertEquals("12.8", item.get("value"), "最新值必须取有日期的观测");
  136. }
  137. /** 验收标准 #8:quickLookup 对 status=0 的 definition 一律剔除,不降级为「指标」+id 假名称 */
  138. @Test
  139. void quickLookup_excludesStatus0Definitions() {
  140. IndicatorDefinition active = def(1L, "双歧杆菌属", "higher_is_better", "%");
  141. IndicatorDefinition dead = def(2L, "废弃指标", "higher_is_better", "U");
  142. dead.setStatus(0);
  143. // loadActiveDefinitions 仅返回 status=1 的定义(模拟 DB 过滤)
  144. when(changeDefinitionMapper.selectList(any())).thenReturn(new ArrayList<>(Arrays.asList(active)));
  145. List<IndicatorValue> rows = new ArrayList<>();
  146. rows.add(value(1L, "2026-10-01", "12.8"));
  147. rows.add(value(2L, "2026-10-01", "5.5")); // status=0 → 必须剔除
  148. when(valueMapper.selectList(any())).thenReturn(rows, new ArrayList<>());
  149. Map<String, Object> data = service.quickLookup(3L, null, 1, 30, 2L, false, 8);
  150. List<?> items = (List<?>) data.get("items");
  151. assertEquals(1, items.size(), "status=0 指标不得出现在结果中");
  152. Map<?, ?> item = (Map<?, ?>) items.get(0);
  153. assertEquals("双歧杆菌属", item.get("name"));
  154. assertFalse(String.valueOf(item.get("name")).startsWith("指标"), "不得生成「指标」+id 的假名称");
  155. }
  156. /** 验收标准 #9:magnitude 排序中 deltaPct=null 的项(基数为 0)恒定排最后 */
  157. @Test
  158. void changeRanking_magnitudeDeltaPctNullAlwaysLast() {
  159. // 三个定义:正常幅度 10%、轻微 1%、基数为 0(deltaPct=null)
  160. IndicatorDefinition d1 = def(1L, "指标A", "higher_is_better", "%");
  161. IndicatorDefinition d2 = def(2L, "指标B", "higher_is_better", "%");
  162. IndicatorDefinition d3 = def(3L, "指标C", "higher_is_better", "%");
  163. d3.setRefRange("0.00-1.00");
  164. when(changeDefinitionMapper.selectList(any()))
  165. .thenReturn(new ArrayList<>(Arrays.asList(d1, d2, d3)));
  166. List<IndicatorValue> rows = new ArrayList<>();
  167. rows.add(value(1L, "2026-08-01", "100"));
  168. rows.add(value(1L, "2026-10-01", "110")); // deltaPct = 10%
  169. rows.add(value(2L, "2026-08-01", "100"));
  170. rows.add(value(2L, "2026-10-01", "101")); // deltaPct = 1%
  171. rows.add(value(3L, "2026-08-01", "0"));
  172. rows.add(value(3L, "2026-10-01", "5")); // 基数为 0 → deltaPct=null
  173. when(valueMapper.selectList(any())).thenReturn(rows);
  174. Map<String, Object> data = service.changeRanking(3L, null, null, "adjacent", "magnitude", 50, 2L,
  175. null, null, null, null, null);
  176. List<Map<?, ?>> items = (List<Map<?, ?>>) data.get("items");
  177. assertEquals(3, items.size());
  178. assertEquals("指标A", items.get(0).get("name"), "|10%| 最大,应排第一");
  179. assertEquals("指标B", items.get(1).get("name"), "|1%| 次之");
  180. assertEquals("指标C", items.get(2).get("name"), "deltaPct=null 恒定排最后");
  181. assertNull(items.get(2).get("deltaPct"), "基数为 0 的百分比必须为 null");
  182. }
  183. }