Skip to content

Commit 9e5dfbc

Browse files
authored
[Test] 로드맵 테스트 보강 (#387)
* test: 로드맵 노드 집계 로직 테스트 추가 * test: 테스트 케이스 추가 * test: 트리구조 구성 테스트 보강
1 parent abf6b3f commit 9e5dfbc

File tree

4 files changed

+550
-8
lines changed

4 files changed

+550
-8
lines changed

back/src/main/java/com/back/domain/roadmap/roadmap/service/JobRoadmapIntegrationServiceV3.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ public class JobRoadmapIntegrationServiceV3 {
4444
private static final double QUALITY_NODE_COUNT_WEIGHT = 0.3; // 품질 점수: 노드 개수 가중치
4545
private static final double QUALITY_STANDARDIZATION_WEIGHT = 0.7; // 품질 점수: 표준화율 가중치
4646

47-
48-
// ========================================
49-
// Public API
50-
// ========================================
51-
5247
/**
5348
* 직업 로드맵 통합 (DB 커넥션 점유 시간 감소를 위해 AI 호출을 트랜잭션 밖에서 수행)
5449
*
@@ -67,7 +62,7 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
6762
// 3. Task prefetch (트랜잭션 외부, 읽기만 수행)
6863
Map<Long, Task> taskMap = prefetchTasks(aggregation);
6964

70-
// 4. 트리 구성 및 AI 호출 (트랜잭션 외부, 6-10분 소요)
65+
// 4. 트리 구성 및 AI 호출 (트랜잭션 외부)
7166
// 이 시간 동안 DB 커넥션은 사용하지 않음
7267
RoadmapTreeBuilder.TreeBuildResult treeResult = roadmapTreeBuilder.build(aggregation, taskMap);
7368

back/src/main/java/com/back/domain/roadmap/roadmap/service/RoadmapTreeBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ private Map<String, RoadmapNode> buildNodesFromIntegratedTexts(
149149
.learningAdvice(integratedTexts != null ? integratedTexts.learningAdvice() : null)
150150
.recommendedResources(integratedTexts != null ? integratedTexts.recommendedResources() : null)
151151
.learningGoals(integratedTexts != null ? integratedTexts.learningGoals() : null)
152-
.difficulty(avgDifficulty != null ? avgDifficulty.intValue() : null)
153-
.importance(avgImportance != null ? avgImportance.intValue() : null)
152+
.difficulty(avgDifficulty != null ? (int) Math.round(avgDifficulty) : null)
153+
.importance(avgImportance != null ? (int) Math.round(avgImportance) : null)
154154
.estimatedHours(avgEstimatedHours)
155155
.task(aggNode.task != null ? taskMap.get(aggNode.task.getId()) : null)
156156
.roadmapId(0L) // 임시 값
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
2+
package com.back.domain.roadmap.roadmap.service;
3+
4+
import com.back.domain.member.member.entity.Member;
5+
import com.back.domain.member.mentor.entity.Mentor;
6+
import com.back.domain.roadmap.roadmap.entity.MentorRoadmap;
7+
import com.back.domain.roadmap.roadmap.entity.RoadmapNode;
8+
import com.back.domain.roadmap.task.entity.Task;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Test;
12+
13+
import java.lang.reflect.Field;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
import java.util.Map;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
class RoadmapAggregatorTest {
21+
22+
private RoadmapAggregator roadmapAggregator;
23+
24+
// --- Mock Data ---
25+
private Mentor mentor1, mentor2, mentor3;
26+
private Task taskJava, taskSpring, taskJpa, taskDocker;
27+
28+
@BeforeEach
29+
void setUp() {
30+
roadmapAggregator = new RoadmapAggregator();
31+
32+
mentor1 = createMentor(101L);
33+
mentor2 = createMentor(102L);
34+
mentor3 = createMentor(103L);
35+
36+
taskJava = createTask(1L, "Java");
37+
taskSpring = createTask(2L, "Spring");
38+
taskJpa = createTask(3L, "JPA");
39+
taskDocker = createTask(4L, "Docker");
40+
}
41+
42+
@Test
43+
@DisplayName("다양한 멘토 로드맵을 집계하여 모든 필드의 통계를 정확하게 생성한다.")
44+
void aggregate_complexScenario_aggregatesAllFieldsCorrectly() {
45+
// given
46+
List<MentorRoadmap> mentorRoadmaps = createComplexMentorRoadmaps();
47+
48+
// when
49+
RoadmapAggregator.AggregationResult result = roadmapAggregator.aggregate(mentorRoadmaps);
50+
51+
// then
52+
assertThat(result.getTotalMentorCount()).isEqualTo(3);
53+
54+
verifyAggregatedNodes(result.getAgg());
55+
verifyRootCandidates(result.getRootCount());
56+
verifyTransitions(result.getTransitions());
57+
verifyMentorAppearance(result.getMentorAppearSet());
58+
verifyNodePositions(result.getPositions());
59+
verifyDescriptionCollections(result.getDescriptions());
60+
}
61+
62+
@Test
63+
@DisplayName("멘토 로드맵이 비어 있을 경우 빈 결과를 반환한다.")
64+
void aggregate_emptyList_returnsEmptyResult() {
65+
// when
66+
RoadmapAggregator.AggregationResult result = roadmapAggregator.aggregate(List.of());
67+
68+
// then
69+
assertThat(result.getTotalMentorCount()).isEqualTo(0);
70+
assertThat(result.getAgg()).isEmpty();
71+
assertThat(result.getRootCount()).isEmpty();
72+
assertThat(result.getTransitions()).isEmpty();
73+
assertThat(result.getMentorAppearSet()).isEmpty();
74+
}
75+
76+
@Test
77+
@DisplayName("노드가 비어 있는 멘토 로드맵은 통계에 반영되지 않는다.")
78+
void aggregate_emptyNodesInRoadmap_areIgnored() {
79+
// given
80+
MentorRoadmap emptyRoadmap = createMentorRoadmap(1L, mentor1, "빈 로드맵");
81+
// 노드를 추가하지 않음
82+
83+
// when
84+
RoadmapAggregator.AggregationResult result = roadmapAggregator.aggregate(List.of(emptyRoadmap));
85+
86+
// then
87+
assertThat(result.getTotalMentorCount()).isEqualTo(1);
88+
assertThat(result.getAgg()).isEmpty();
89+
assertThat(result.getRootCount()).isEmpty();
90+
assertThat(result.getTransitions()).isEmpty();
91+
}
92+
93+
@Test
94+
@DisplayName("Task와 TaskName이 모두 없는 노드를 처리할 수 있다.")
95+
void aggregate_nodeWithNoTaskOrName_generatesUnknownKey() {
96+
// given
97+
MentorRoadmap roadmap = createMentorRoadmap(1L, mentor1, "멘토1 로드맵");
98+
RoadmapNode unknownNode = RoadmapNode.builder()
99+
.roadmapId(roadmap.getId())
100+
.roadmapType(RoadmapNode.RoadmapType.MENTOR)
101+
.stepOrder(1)
102+
.task(null)
103+
.taskName(null)
104+
.build();
105+
roadmap.addNodes(List.of(unknownNode));
106+
107+
// when
108+
RoadmapAggregator.AggregationResult result = roadmapAggregator.aggregate(List.of(roadmap));
109+
110+
// then
111+
assertThat(result.getAgg()).containsKey("N:__unknown__");
112+
assertThat(result.getAgg().get("N:__unknown__").count).isEqualTo(1);
113+
assertThat(result.getRootCount()).containsEntry("N:__unknown__", 1);
114+
}
115+
116+
@Test
117+
@DisplayName("하나의 로드맵에서 동일 Task가 여러 번 등장할 경우 누적되어야 한다.")
118+
void aggregate_sameTaskMultipleTimes_inSingleRoadmap_countsAccumulated() {
119+
// given
120+
MentorRoadmap roadmap = createMentorRoadmap(1L, mentor1, "중복 Task 로드맵");
121+
RoadmapNode node1 = createStandardNode(roadmap.getId(), 1, taskJava, null, null, null, null, null, null);
122+
RoadmapNode node2 = createStandardNode(roadmap.getId(), 2, taskJava, null, null, null, null, null, null);
123+
roadmap.addNodes(List.of(node1, node2));
124+
125+
// when
126+
RoadmapAggregator.AggregationResult result = roadmapAggregator.aggregate(List.of(roadmap));
127+
128+
// then
129+
assertThat(result.getAgg()).containsKey("T:1");
130+
assertThat(result.getAgg().get("T:1").count).isEqualTo(2); // count 누적
131+
assertThat(result.getPositions().get("T:1")).containsExactlyInAnyOrder(1, 2);
132+
assertThat(result.getMentorAppearSet().get("T:1")).containsExactly(mentor1.getId());
133+
}
134+
135+
@Test
136+
@DisplayName("여러 멘토가 동일한 전이(Transition)를 가질 경우 카운트가 누적된다.")
137+
void aggregate_duplicateTransitions_areCounted() {
138+
// given: 두 멘토가 모두 Java -> Spring 로드맵을 가짐
139+
MentorRoadmap roadmap1 = createMentorRoadmap(1L, mentor1, "로드맵1");
140+
roadmap1.addNodes(List.of(
141+
createStandardNode(roadmap1.getId(), 1, taskJava, null, null, null, null, null, null),
142+
createStandardNode(roadmap1.getId(), 2, taskSpring, null, null, null, null, null, null)
143+
));
144+
145+
MentorRoadmap roadmap2 = createMentorRoadmap(2L, mentor2, "로드맵2");
146+
roadmap2.addNodes(List.of(
147+
createStandardNode(roadmap2.getId(), 1, taskJava, null, null, null, null, null, null),
148+
createStandardNode(roadmap2.getId(), 2, taskSpring, null, null, null, null, null, null)
149+
));
150+
151+
// when
152+
RoadmapAggregator.AggregationResult result = roadmapAggregator.aggregate(List.of(roadmap1, roadmap2));
153+
154+
// then
155+
assertThat(result.getTransitions().get("T:1")).containsEntry("T:2", 2);
156+
assertThat(result.getAgg().get("T:1").count).isEqualTo(2);
157+
assertThat(result.getAgg().get("T:2").count).isEqualTo(2);
158+
}
159+
160+
private void verifyAggregatedNodes(Map<String, RoadmapAggregator.AggregatedNode> agg) {
161+
assertThat(agg).hasSize(5);
162+
assertThat(agg.get("T:1").count).isEqualTo(3);
163+
assertThat(agg.get("T:1").displayName).isEqualTo("Java");
164+
assertThat(agg.get("T:2").count).isEqualTo(2);
165+
assertThat(agg.get("T:2").displayName).isEqualTo("Spring");
166+
assertThat(agg.get("T:3").count).isEqualTo(1);
167+
assertThat(agg.get("T:3").displayName).isEqualTo("JPA");
168+
assertThat(agg.get("T:4").count).isEqualTo(1);
169+
assertThat(agg.get("T:4").displayName).isEqualTo("Docker");
170+
assertThat(agg.get("N:custom db task").count).isEqualTo(1);
171+
assertThat(agg.get("N:custom db task").displayName).isEqualTo("Custom DB Task");
172+
}
173+
174+
private void verifyRootCandidates(Map<String, Integer> rootCount) {
175+
assertThat(rootCount).hasSize(2);
176+
assertThat(rootCount.get("T:1")).isEqualTo(2);
177+
assertThat(rootCount.get("T:4")).isEqualTo(1);
178+
}
179+
180+
private void verifyTransitions(Map<String, Map<String, Integer>> transitions) {
181+
assertThat(transitions.get("T:1")).containsEntry("T:2", 1).containsEntry("T:3", 1);
182+
assertThat(transitions.get("T:2")).containsEntry("N:custom db task", 1);
183+
assertThat(transitions.get("T:3")).containsEntry("T:2", 1);
184+
assertThat(transitions.get("T:4")).containsEntry("T:1", 1);
185+
}
186+
187+
private void verifyMentorAppearance(Map<String, java.util.Set<Long>> mentorAppearSet) {
188+
assertThat(mentorAppearSet.get("T:1")).containsExactlyInAnyOrder(101L, 102L, 103L);
189+
assertThat(mentorAppearSet.get("T:2")).containsExactlyInAnyOrder(101L, 102L);
190+
assertThat(mentorAppearSet.get("T:3")).containsExactlyInAnyOrder(102L);
191+
assertThat(mentorAppearSet.get("T:4")).containsExactlyInAnyOrder(103L);
192+
assertThat(mentorAppearSet.get("N:custom db task")).containsExactlyInAnyOrder(101L);
193+
}
194+
195+
private void verifyNodePositions(Map<String, List<Integer>> positions) {
196+
assertThat(positions.get("T:1")).containsExactlyInAnyOrder(1, 1, 2);
197+
assertThat(positions.get("T:2")).containsExactlyInAnyOrder(2, 3);
198+
assertThat(positions.get("T:3")).containsExactlyInAnyOrder(2);
199+
assertThat(positions.get("T:4")).containsExactlyInAnyOrder(1);
200+
assertThat(positions.get("N:custom db task")).containsExactlyInAnyOrder(3);
201+
}
202+
203+
private void verifyDescriptionCollections(RoadmapAggregator.DescriptionCollections descriptions) {
204+
assertThat(descriptions.getLearningAdvices().get("T:1"))
205+
.containsExactlyInAnyOrder("Java Advice from Mentor1", "Java Advice from Mentor2");
206+
assertThat(descriptions.getRecommendedResources().get("T:2"))
207+
.containsExactly("Spring Resource from Mentor1");
208+
assertThat(descriptions.getLearningGoals().get("T:1"))
209+
.containsExactly("Java Goal from Mentor2");
210+
assertThat(descriptions.getDifficulties().get("T:1"))
211+
.containsExactlyInAnyOrder(2, 3);
212+
assertThat(descriptions.getImportances().get("T:2"))
213+
.containsExactlyInAnyOrder(5, 4);
214+
assertThat(descriptions.getEstimatedHours().get("T:1"))
215+
.containsExactly(40);
216+
}
217+
218+
// --- Helper Methods to build mock data (수정된 헬퍼) ---
219+
220+
private List<MentorRoadmap> createComplexMentorRoadmaps() {
221+
// Mentor 1: Java -> Spring -> Custom DB Task
222+
MentorRoadmap roadmap1 = createMentorRoadmap(1L, mentor1, "멘토1 로드맵");
223+
RoadmapNode node1_1 = createStandardNode(roadmap1.getId(), 1, taskJava, "Java Advice from Mentor1", null, null, 2, 5, 40);
224+
RoadmapNode node1_2 = createStandardNode(roadmap1.getId(), 2, taskSpring, null, "Spring Resource from Mentor1", null, 4, 5, 80);
225+
RoadmapNode node1_3 = createCustomNode(roadmap1.getId(), 3, "Custom DB Task", null, null, null, 3, 3, 20);
226+
roadmap1.addNodes(Arrays.asList(node1_1, node1_2, node1_3));
227+
228+
// Mentor 2: Java -> JPA -> Spring
229+
MentorRoadmap roadmap2 = createMentorRoadmap(2L, mentor2, "멘토2 로드맵");
230+
RoadmapNode node2_1 = createStandardNode(roadmap2.getId(), 1, taskJava, "Java Advice from Mentor2", null, "Java Goal from Mentor2", 3, 4, null);
231+
RoadmapNode node2_2 = createStandardNode(roadmap2.getId(), 2, taskJpa, null, null, "JPA Goal", 3, 5, 60);
232+
RoadmapNode node2_3 = createStandardNode(roadmap2.getId(), 3, taskSpring, null, null, null, 5, 4, 100);
233+
roadmap2.addNodes(Arrays.asList(node2_1, node2_2, node2_3));
234+
235+
// Mentor 3: Docker -> Java
236+
MentorRoadmap roadmap3 = createMentorRoadmap(3L, mentor3, "멘토3 로드맵");
237+
RoadmapNode node3_1 = createStandardNode(roadmap3.getId(), 1, taskDocker, null, null, null, 2, 3, 20);
238+
RoadmapNode node3_2 = createStandardNode(roadmap3.getId(), 2, taskJava, null, null, null, null, null, null);
239+
roadmap3.addNodes(Arrays.asList(node3_1, node3_2));
240+
241+
return Arrays.asList(roadmap1, roadmap2, roadmap3);
242+
}
243+
244+
private Mentor createMentor(Long id) {
245+
Member member = new Member(id, "mentor" + id + "@test.com", "테스트멘토" + id, "테스트멘토" + id, Member.Role.MENTOR);
246+
Mentor mentor = Mentor.builder().member(member).job(null).build();
247+
try {
248+
Field idField = mentor.getClass().getSuperclass().getDeclaredField("id");
249+
idField.setAccessible(true);
250+
idField.set(mentor, id);
251+
} catch (NoSuchFieldException | IllegalAccessException e) {
252+
throw new RuntimeException(e);
253+
}
254+
return mentor;
255+
}
256+
257+
private Task createTask(Long id, String name) {
258+
Task task = new Task(name);
259+
try {
260+
Field idField = task.getClass().getSuperclass().getDeclaredField("id");
261+
idField.setAccessible(true);
262+
idField.set(task, id);
263+
} catch (NoSuchFieldException | IllegalAccessException e) {
264+
throw new RuntimeException(e);
265+
}
266+
return task;
267+
}
268+
269+
private MentorRoadmap createMentorRoadmap(Long id, Mentor mentor, String title) {
270+
MentorRoadmap roadmap = new MentorRoadmap(mentor, title, "테스트 설명");
271+
try {
272+
Field idField = roadmap.getClass().getSuperclass().getDeclaredField("id");
273+
idField.setAccessible(true);
274+
idField.set(roadmap, id);
275+
} catch (NoSuchFieldException | IllegalAccessException e) {
276+
throw new RuntimeException(e);
277+
}
278+
return roadmap;
279+
}
280+
281+
private RoadmapNode createStandardNode(Long roadmapId, int order, Task task, String advice, String resource, String goal, Integer difficulty, Integer importance, Integer estimatedHours) {
282+
return RoadmapNode.builder()
283+
.roadmapId(roadmapId) // ★★★★★ FIX: 소속될 로드맵 ID 설정
284+
.roadmapType(RoadmapNode.RoadmapType.MENTOR) // ★★★★★ FIX: 타입 명시
285+
.stepOrder(order)
286+
.task(task)
287+
.taskName(task.getName())
288+
.learningAdvice(advice)
289+
.recommendedResources(resource)
290+
.learningGoals(goal)
291+
.difficulty(difficulty)
292+
.importance(importance)
293+
.estimatedHours(estimatedHours)
294+
.build();
295+
}
296+
297+
private RoadmapNode createCustomNode(Long roadmapId, int order, String taskName, String advice, String resource, String goal, Integer difficulty, Integer importance, Integer estimatedHours) {
298+
return RoadmapNode.builder()
299+
.roadmapId(roadmapId)
300+
.roadmapType(RoadmapNode.RoadmapType.MENTOR)
301+
.stepOrder(order)
302+
.task(null)
303+
.taskName(taskName)
304+
.learningAdvice(advice)
305+
.recommendedResources(resource)
306+
.learningGoals(goal)
307+
.difficulty(difficulty)
308+
.importance(importance)
309+
.estimatedHours(estimatedHours)
310+
.build();
311+
}
312+
}

0 commit comments

Comments
 (0)