Skip to content

Commit 4256f41

Browse files
committed
test: 트리구조 구성 테스트 보강
1 parent 6efcc0f commit 4256f41

File tree

2 files changed

+237
-2
lines changed

2 files changed

+237
-2
lines changed

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: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
package com.back.domain.roadmap.roadmap.service;
2+
3+
import com.back.domain.roadmap.roadmap.dto.response.TextFieldIntegrationResponse;
4+
import com.back.domain.roadmap.roadmap.entity.RoadmapNode;
5+
import com.back.domain.roadmap.task.entity.Task;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.extension.ExtendWith;
10+
import org.mockito.InjectMocks;
11+
import org.mockito.Mock;
12+
import org.mockito.junit.jupiter.MockitoExtension;
13+
14+
import java.lang.reflect.Field;
15+
import java.util.*;
16+
import java.util.stream.Collectors;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.when;
21+
22+
@ExtendWith(MockitoExtension.class)
23+
class RoadmapTreeBuilderTest {
24+
25+
@Mock
26+
private TextFieldIntegrationService textFieldIntegrationService;
27+
28+
@InjectMocks
29+
private RoadmapTreeBuilder roadmapTreeBuilder;
30+
31+
private Map<Long, Task> taskMap;
32+
private Task taskJava, taskSpring, taskJpa, taskDocker, taskMysql;
33+
34+
@BeforeEach
35+
void setUp() {
36+
taskJava = createTask(1L, "Java");
37+
taskSpring = createTask(2L, "Spring");
38+
taskJpa = createTask(3L, "JPA");
39+
taskDocker = createTask(4L, "Docker");
40+
taskMysql = createTask(5L, "MySQL");
41+
42+
taskMap = Map.of(
43+
1L, taskJava,
44+
2L, taskSpring,
45+
3L, taskJpa,
46+
4L, taskDocker,
47+
5L, taskMysql
48+
);
49+
}
50+
51+
@Test
52+
@DisplayName("복잡한 통계 데이터를 기반으로 올바른 로드맵 트리를 생성한다.")
53+
void build_complexScenario_constructsCorrectTree() {
54+
// given
55+
RoadmapAggregator.AggregationResult aggregation = createComplexAggregationResult();
56+
mockTextFieldIntegration();
57+
58+
// when
59+
RoadmapTreeBuilder.TreeBuildResult result = roadmapTreeBuilder.build(aggregation, taskMap);
60+
61+
// then
62+
// 1. 루트 노드 검증
63+
assertThat(result.getRootKey()).isEqualTo("T:1"); // Java가 루트여야 함
64+
RoadmapNode root = result.getKeyToNode().get("T:1");
65+
assertThat(root).isNotNull();
66+
assertThat(root.getTaskName()).isEqualTo("Java");
67+
assertThat(root.getLevel()).isEqualTo(0);
68+
assertThat(root.getStepOrder()).isEqualTo(1);
69+
assertThat(root.getDifficulty()).isEqualTo(3); // (2+3)/2 = 2.5 -> 3
70+
assertThat(root.getImportance()).isEqualTo(5); // (5+4)/2 = 4.5 -> 5
71+
72+
// 2. 생성된 노드 속성 검증 (AI 통합 필드)
73+
assertThat(root.getLearningAdvice()).isEqualTo("Integrated Java Advice");
74+
assertThat(root.getRecommendedResources()).isEqualTo("Integrated Java Resources");
75+
76+
// 3. 트리 구조 검증 (BFS 결과)
77+
// Java(T:1)의 자식은 Spring(T:2)과 JPA(T:3)여야 함
78+
assertThat(root.getChildren()).hasSize(2);
79+
List<String> childrenNames = root.getChildren().stream()
80+
.map(RoadmapNode::getTaskName)
81+
.collect(Collectors.toList());
82+
assertThat(childrenNames).containsExactlyInAnyOrder("Spring", "JPA");
83+
84+
RoadmapNode springNode = findChildByName(root, "Spring");
85+
RoadmapNode jpaNode = findChildByName(root, "JPA");
86+
87+
assertThat(springNode).isNotNull();
88+
assertThat(springNode.getLevel()).isEqualTo(1);
89+
assertThat(springNode.getParent()).isEqualTo(root);
90+
91+
assertThat(jpaNode).isNotNull();
92+
assertThat(jpaNode.getLevel()).isEqualTo(1);
93+
assertThat(jpaNode.getParent()).isEqualTo(root);
94+
95+
// Spring(T:2)의 자식은 Docker(T:4)여야 함
96+
assertThat(springNode.getChildren()).hasSize(1);
97+
RoadmapNode dockerNode = springNode.getChildren().get(0);
98+
assertThat(dockerNode.getTaskName()).isEqualTo("Docker");
99+
assertThat(dockerNode.getLevel()).isEqualTo(2);
100+
assertThat(dockerNode.getParent()).isEqualTo(springNode);
101+
102+
// 4. 순환 및 최적 부모 로직 검증
103+
// JPA(T:3)는 Spring(T:2)으로 가는 전이가 있지만, Spring의 최적 부모는 Java(T:1)이므로
104+
// JPA는 Spring을 자식으로 가지면 안됨.
105+
assertThat(jpaNode.getChildren()).isEmpty();
106+
107+
// 5. 방문하지 않은 노드 검증 (MySQL은 어디에도 연결되지 않음)
108+
assertThat(result.getVisited()).doesNotContain("T:5");
109+
}
110+
111+
@Test
112+
@DisplayName("rootCount가 비어있을 경우 전체 노드 등장 빈도를 기반으로 루트를 선택한다.")
113+
void build_whenRootCountIsEmpty_selectsRootFromOverallCount() {
114+
// given
115+
RoadmapAggregator.AggregationResult aggregation = createComplexAggregationResult();
116+
aggregation.rootCount.clear(); // 루트 카운트 강제 클리어
117+
mockTextFieldIntegration();
118+
119+
// when
120+
RoadmapTreeBuilder.TreeBuildResult result = roadmapTreeBuilder.build(aggregation, taskMap);
121+
122+
// then
123+
// agg.count가 가장 높은 Java(4)가 루트가 되어야 함
124+
assertThat(result.getRootKey()).isEqualTo("T:1");
125+
}
126+
127+
@Test
128+
@DisplayName("TextFieldIntegrationService 호출 실패 시 빈 텍스트로 트리를 생성한다.")
129+
void build_whenTextFieldIntegrationFails_proceedsWithEmptyTexts() {
130+
// given
131+
RoadmapAggregator.AggregationResult aggregation = createComplexAggregationResult();
132+
// AI 서비스 Mock이 예외를 던지도록 설정
133+
when(textFieldIntegrationService.integrateBatch(any()))
134+
.thenThrow(new RuntimeException("AI service is down"));
135+
136+
// when
137+
RoadmapTreeBuilder.TreeBuildResult result = roadmapTreeBuilder.build(aggregation, taskMap);
138+
139+
// then
140+
// 예외가 발생해도 빌드는 중단되지 않아야 함
141+
assertThat(result).isNotNull();
142+
RoadmapNode root = result.getKeyToNode().get("T:1");
143+
assertThat(root).isNotNull();
144+
145+
// 텍스트 필드는 모두 null이어야 함
146+
assertThat(root.getLearningAdvice()).isNull();
147+
assertThat(root.getRecommendedResources()).isNull();
148+
assertThat(root.getLearningGoals()).isNull();
149+
}
150+
151+
152+
// --- Helper Methods ---
153+
154+
private RoadmapAggregator.AggregationResult createComplexAggregationResult() {
155+
RoadmapAggregator.AggregationResult result = new RoadmapAggregator.AggregationResult(4);
156+
157+
// 1. agg (노드 등장 횟수)
158+
result.agg.put("T:1", new RoadmapAggregator.AggregatedNode(taskJava, "Java"));
159+
result.agg.get("T:1").count = 4;
160+
result.agg.put("T:2", new RoadmapAggregator.AggregatedNode(taskSpring, "Spring"));
161+
result.agg.get("T:2").count = 3;
162+
result.agg.put("T:3", new RoadmapAggregator.AggregatedNode(taskJpa, "JPA"));
163+
result.agg.get("T:3").count = 2;
164+
result.agg.put("T:4", new RoadmapAggregator.AggregatedNode(taskDocker, "Docker"));
165+
result.agg.get("T:4").count = 1;
166+
result.agg.put("T:5", new RoadmapAggregator.AggregatedNode(taskMysql, "MySQL"));
167+
result.agg.get("T:5").count = 1;
168+
169+
170+
// 2. rootCount (루트 노드 빈도)
171+
result.rootCount.put("T:1", 3); // Java가 압도적인 루트
172+
result.rootCount.put("T:4", 1);
173+
174+
// 3. transitions (전이)
175+
// Java -> Spring (3번)
176+
// Java -> JPA (2번)
177+
// Spring -> Docker (1번)
178+
// JPA -> Spring (1번) - 순환 구조 및 부모 경쟁 유발
179+
result.transitions.put("T:1", new HashMap<>(Map.of("T:2", 3, "T:3", 2)));
180+
result.transitions.put("T:2", new HashMap<>(Map.of("T:4", 1)));
181+
result.transitions.put("T:3", new HashMap<>(Map.of("T:2", 1)));
182+
183+
184+
// 4. positions (평균 위치)
185+
result.positions.put("T:1", List.of(1, 1, 1, 2)); // avg ~1.25
186+
result.positions.put("T:2", List.of(2, 2, 3)); // avg ~2.33
187+
result.positions.put("T:3", List.of(2, 3)); // avg ~2.5
188+
result.positions.put("T:4", List.of(3)); // avg 3.0
189+
190+
// 5. mentorAppearSet (멘토 커버리지)
191+
result.mentorAppearSet.put("T:1", Set.of(101L, 102L, 103L, 104L)); // 4/4
192+
result.mentorAppearSet.put("T:2", Set.of(101L, 102L, 103L)); // 3/4
193+
result.mentorAppearSet.put("T:3", Set.of(101L, 102L)); // 2/4
194+
result.mentorAppearSet.put("T:4", Set.of(101L)); // 1/4
195+
196+
// 6. descriptions (상세 정보)
197+
result.descriptions.learningAdvices.put("T:1", List.of("Java advice 1", "Java advice 2"));
198+
result.descriptions.recommendedResources.put("T:1", List.of("Java resource 1"));
199+
result.descriptions.difficulties.put("T:1", List.of(2, 3)); // avg 2.5
200+
result.descriptions.importances.put("T:1", List.of(5, 4)); // avg 4.5
201+
result.descriptions.estimatedHours.put("T:2", List.of(40, 60, 80)); // avg 60
202+
203+
return result;
204+
}
205+
206+
private void mockTextFieldIntegration() {
207+
TextFieldIntegrationResponse javaResponse = new TextFieldIntegrationResponse(
208+
"Integrated Java Advice",
209+
"Integrated Java Resources",
210+
"Integrated Java Goals"
211+
);
212+
// AI 서비스 Mocking
213+
when(textFieldIntegrationService.integrateBatch(any()))
214+
.thenReturn(Map.of("T:1", javaResponse));
215+
}
216+
217+
private Task createTask(Long id, String name) {
218+
Task task = new Task(name);
219+
try {
220+
Field idField = task.getClass().getSuperclass().getDeclaredField("id");
221+
idField.setAccessible(true);
222+
idField.set(task, id);
223+
} catch (NoSuchFieldException | IllegalAccessException e) {
224+
throw new RuntimeException(e);
225+
}
226+
return task;
227+
}
228+
229+
private RoadmapNode findChildByName(RoadmapNode parent, String name) {
230+
return parent.getChildren().stream()
231+
.filter(node -> name.equals(node.getTaskName()))
232+
.findFirst()
233+
.orElse(null);
234+
}
235+
}

0 commit comments

Comments
 (0)