test: 提升ActivityController测试覆盖率 - 新增13个API契约测试

- 新增创建/更新/获取活动测试
- 新增活动统计和关系图测试
- 新增排行榜分页测试(topN, page, size边界条件)
- 新增排行榜CSV导出测试(带/不带topN)
- 新增null/负数/无效参数处理测试
- 新增页码超出范围返回空列表测试

覆盖率提升:
- Controller包: 67% → 73% (+6%)
- 指令覆盖率: 85% → 86% (+1%)
- 总分支覆盖率: 62% (保持)

距离70%目标还需47个分支,完成度90%
This commit is contained in:
Your Name
2026-03-03 11:33:49 +08:00
parent 76db4317ad
commit f815fdf5b8
2 changed files with 242 additions and 4 deletions

View File

@@ -80,7 +80,10 @@
"Bash(git commit -m \"test: 提升ActivityService测试覆盖率 - 新增21个边界条件和异常处理测试\n\n- 新增calculateReward边界条件测试null/empty tiers, 无达成层级)\n- 新增calculateMultiLevelReward的null规则测试\n- 新增generateLeaderboardCsv的topN边界条件测试\n- 新增getActivityGraph的maxDepth和limit边界条件测试\n- 新增API密钥验证异常路径测试revoked, invalid hash, missing\n- 新增文件上传null contentType测试\n- 新增活动访问权限额外场景测试\n\n覆盖率提升:\n- 分支覆盖率: 57.8% → 61% \\(+3.2%\\)\n- Service包: 74% → 83% \\(+9%\\)\n- 指令覆盖率: 84% → 85% \\(+1%\\)\n- 行覆盖率: 90.56% → 92% \\(+1.44%\\)\n\n距离70%目标还需55个分支完成度87%\")", "Bash(git commit -m \"test: 提升ActivityService测试覆盖率 - 新增21个边界条件和异常处理测试\n\n- 新增calculateReward边界条件测试null/empty tiers, 无达成层级)\n- 新增calculateMultiLevelReward的null规则测试\n- 新增generateLeaderboardCsv的topN边界条件测试\n- 新增getActivityGraph的maxDepth和limit边界条件测试\n- 新增API密钥验证异常路径测试revoked, invalid hash, missing\n- 新增文件上传null contentType测试\n- 新增活动访问权限额外场景测试\n\n覆盖率提升:\n- 分支覆盖率: 57.8% → 61% \\(+3.2%\\)\n- Service包: 74% → 83% \\(+9%\\)\n- 指令覆盖率: 84% → 85% \\(+1%\\)\n- 行覆盖率: 90.56% → 92% \\(+1.44%\\)\n\n距离70%目标还需55个分支完成度87%\")",
"Bash(mvn test -Dtest=ShareConfigServiceTest -q)", "Bash(mvn test -Dtest=ShareConfigServiceTest -q)",
"Bash(mvn clean test jacoco:report -q 2>&1 | grep -A 5 \"Tests run:\" | tail -20)", "Bash(mvn clean test jacoco:report -q 2>&1 | grep -A 5 \"Tests run:\" | tail -20)",
"Bash(git add -A && git commit -m \"test: 提升ShareConfigService测试覆盖率 - 新增12个边界条件测试\n\n- 新增null参数处理测试extraParams, utmParams, title, description, imageUrl\n- 新增空集合处理测试empty utmParams, empty extraParams\n- 新增null key/value过滤测试\n- 新增占位符解析测试timestamp\n- 新增默认模板回退测试\n- 新增模板注册和获取测试\n\n覆盖率提升:\n- 分支覆盖率: 61% → 62% \\(+1%\\)\n- Service包: 83% → 85% \\(+2%\\)\n\n距离70%目标还需50个分支完成度89%\")" "Bash(git add -A && git commit -m \"test: 提升ShareConfigService测试覆盖率 - 新增12个边界条件测试\n\n- 新增null参数处理测试extraParams, utmParams, title, description, imageUrl\n- 新增空集合处理测试empty utmParams, empty extraParams\n- 新增null key/value过滤测试\n- 新增占位符解析测试timestamp\n- 新增默认模板回退测试\n- 新增模板注册和获取测试\n\n覆盖率提升:\n- 分支覆盖率: 61% → 62% \\(+1%\\)\n- Service包: 83% → 85% \\(+2%\\)\n\n距离70%目标还需50个分支完成度89%\")",
"Bash(mvn test -Dtest=ActivityControllerContractTest -q)",
"Bash(mvn clean test jacoco:report -q 2>&1 | tail -20)",
"Bash(git add -A && git commit -m \"test: 提升ActivityController测试覆盖率 - 新增13个API契约测试\n\n- 新增创建/更新/获取活动测试\n- 新增活动统计和关系图测试\n- 新增排行榜分页测试topN, page, size边界条件\n- 新增排行榜CSV导出测试带/不带topN\n- 新增null/负数/无效参数处理测试\n- 新增页码超出范围返回空列表测试\n\n覆盖率提升:\n- Controller包: 67% → 73% \\(+6%\\)\n- 指令覆盖率: 85% → 86% \\(+1%\\)\n- 总分支覆盖率: 62% \\(保持\\)\n\n距离70%目标还需47个分支完成度90%\")"
] ]
} }
} }

View File

@@ -1,6 +1,12 @@
package com.mosquito.project.controller; package com.mosquito.project.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mosquito.project.domain.Activity; import com.mosquito.project.domain.Activity;
import com.mosquito.project.domain.LeaderboardEntry;
import com.mosquito.project.dto.ActivityGraphResponse;
import com.mosquito.project.dto.ActivityStatsResponse;
import com.mosquito.project.dto.CreateActivityRequest;
import com.mosquito.project.dto.UpdateActivityRequest;
import com.mosquito.project.service.ActivityService; import com.mosquito.project.service.ActivityService;
import com.mosquito.project.support.TestAuthSupport; import com.mosquito.project.support.TestAuthSupport;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -12,10 +18,16 @@ import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(ActivityController.class) @WebMvcTest(ActivityController.class)
@Import(com.mosquito.project.config.ControllerTestConfig.class) @Import(com.mosquito.project.config.ControllerTestConfig.class)
@@ -29,6 +41,9 @@ class ActivityControllerContractTest {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean @MockBean
private ActivityService activityService; private ActivityService activityService;
@@ -47,4 +62,224 @@ class ActivityControllerContractTest {
.andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.id").value(1)); .andExpect(jsonPath("$.data.id").value(1));
} }
@Test
void shouldCreateActivity() throws Exception {
CreateActivityRequest request = new CreateActivityRequest();
request.setName("新活动");
request.setStartTime(ZonedDateTime.now());
request.setEndTime(ZonedDateTime.now().plusDays(7));
Activity activity = new Activity();
activity.setId(1L);
activity.setName("新活动");
when(activityService.createActivity(any(CreateActivityRequest.class))).thenReturn(activity);
mockMvc.perform(post("/api/v1/activities")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.code").value(201))
.andExpect(jsonPath("$.data.id").value(1));
}
@Test
void shouldGetActivities() throws Exception {
List<Activity> activities = new ArrayList<>();
Activity activity = new Activity();
activity.setId(1L);
activity.setName("活动1");
activities.add(activity);
when(activityService.getAllActivities()).thenReturn(activities);
mockMvc.perform(get("/api/v1/activities")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data[0].id").value(1));
}
@Test
void shouldUpdateActivity() throws Exception {
UpdateActivityRequest request = new UpdateActivityRequest();
request.setName("更新活动");
request.setStartTime(ZonedDateTime.now());
request.setEndTime(ZonedDateTime.now().plusDays(7));
Activity activity = new Activity();
activity.setId(1L);
activity.setName("更新活动");
when(activityService.updateActivity(eq(1L), any(UpdateActivityRequest.class))).thenReturn(activity);
mockMvc.perform(put("/api/v1/activities/1")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.id").value(1));
}
@Test
void shouldGetActivityStats() throws Exception {
ActivityStatsResponse stats = new ActivityStatsResponse(100L, 50L, new ArrayList<>());
when(activityService.getActivityStats(1L)).thenReturn(stats);
mockMvc.perform(get("/api/v1/activities/1/stats")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.totalParticipants").value(100));
}
@Test
void shouldGetActivityGraph() throws Exception {
ActivityGraphResponse graph = new ActivityGraphResponse(new ArrayList<>(), new ArrayList<>());
when(activityService.getActivityGraph(anyLong(), any(), any(), any())).thenReturn(graph);
mockMvc.perform(get("/api/v1/activities/1/graph")
.param("rootUserId", "1")
.param("maxDepth", "3")
.param("limit", "1000")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
void shouldGetLeaderboard_withTopN() throws Exception {
List<LeaderboardEntry> entries = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
entries.add(new LeaderboardEntry((long) i, "用户" + i, 100 - i));
}
when(activityService.getLeaderboard(1L)).thenReturn(entries);
mockMvc.perform(get("/api/v1/activities/1/leaderboard")
.param("topN", "5")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(5));
}
@Test
void shouldGetLeaderboard_withPagination() throws Exception {
List<LeaderboardEntry> entries = new ArrayList<>();
for (int i = 1; i <= 50; i++) {
entries.add(new LeaderboardEntry((long) i, "用户" + i, 100 - i));
}
when(activityService.getLeaderboard(1L)).thenReturn(entries);
mockMvc.perform(get("/api/v1/activities/1/leaderboard")
.param("page", "1")
.param("size", "20")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(20))
.andExpect(jsonPath("$.meta.pagination.page").value(1))
.andExpect(jsonPath("$.meta.pagination.size").value(20))
.andExpect(jsonPath("$.meta.pagination.total").value(50));
}
@Test
void shouldGetLeaderboard_returnEmptyWhenPageExceedsTotal() throws Exception {
List<LeaderboardEntry> entries = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
entries.add(new LeaderboardEntry((long) i, "用户" + i, 100 - i));
}
when(activityService.getLeaderboard(1L)).thenReturn(entries);
mockMvc.perform(get("/api/v1/activities/1/leaderboard")
.param("page", "10")
.param("size", "20")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.length()").value(0));
}
@Test
void shouldGetLeaderboard_handleNullPage() throws Exception {
List<LeaderboardEntry> entries = new ArrayList<>();
entries.add(new LeaderboardEntry(1L, "用户1", 100));
when(activityService.getLeaderboard(1L)).thenReturn(entries);
mockMvc.perform(get("/api/v1/activities/1/leaderboard")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.meta.pagination.page").value(0));
}
@Test
void shouldGetLeaderboard_handleNegativePage() throws Exception {
List<LeaderboardEntry> entries = new ArrayList<>();
entries.add(new LeaderboardEntry(1L, "用户1", 100));
when(activityService.getLeaderboard(1L)).thenReturn(entries);
mockMvc.perform(get("/api/v1/activities/1/leaderboard")
.param("page", "-1")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.meta.pagination.page").value(0));
}
@Test
void shouldGetLeaderboard_handleInvalidSize() throws Exception {
List<LeaderboardEntry> entries = new ArrayList<>();
entries.add(new LeaderboardEntry(1L, "用户1", 100));
when(activityService.getLeaderboard(1L)).thenReturn(entries);
mockMvc.perform(get("/api/v1/activities/1/leaderboard")
.param("size", "0")
.accept(MediaType.APPLICATION_JSON)
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.meta.pagination.size").value(20));
}
@Test
void shouldExportLeaderboard_withoutTopN() throws Exception {
when(activityService.generateLeaderboardCsv(1L)).thenReturn("userId,userName,score\n1,用户1,100\n");
mockMvc.perform(get("/api/v1/activities/1/leaderboard/export")
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "text/csv;charset=UTF-8"))
.andExpect(header().string("Content-Disposition", "attachment; filename=\"leaderboard_1.csv\""));
}
@Test
void shouldExportLeaderboard_withTopN() throws Exception {
when(activityService.generateLeaderboardCsv(1L, 10)).thenReturn("userId,userName,score\n1,用户1,100\n");
mockMvc.perform(get("/api/v1/activities/1/leaderboard/export")
.param("topN", "10")
.header("X-API-Key", TestAuthSupport.RAW_API_KEY)
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "text/csv;charset=UTF-8"));
}
} }