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

@@ -1,6 +1,12 @@
package com.mosquito.project.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
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.support.TestAuthSupport;
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.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.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ActivityController.class)
@Import(com.mosquito.project.config.ControllerTestConfig.class)
@@ -29,6 +41,9 @@ class ActivityControllerContractTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private ActivityService activityService;
@@ -47,4 +62,224 @@ class ActivityControllerContractTest {
.andExpect(jsonPath("$.code").value(200))
.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"));
}
}