Skip to content

Commit 87251d8

Browse files
authored
(#49) 도메인 핵심 로직 단위 테스트 추가
* feature: Score, RankInfo, ActivityStatistics, User 단위 테스트 추가 * refactor: 코드 리뷰 반영 - hashCode 검증, 단일 사용자 티어 엣지 케이스, 테스트 이름 수정 * chore: CI 파이프라인에 테스트 실행 단계 추가
1 parent 623d828 commit 87251d8

6 files changed

Lines changed: 751 additions & 13 deletions

File tree

.github/workflows/ci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ jobs:
3333
- name: Build with Gradle
3434
run: ./gradlew build -x test
3535

36+
- name: Run tests
37+
run: ./gradlew test
38+
39+
- name: Upload test results
40+
uses: actions/upload-artifact@v4
41+
if: always()
42+
with:
43+
name: test-results
44+
path: build/reports/tests/test/
45+
retention-days: 7
46+
3647
- name: Upload build artifacts
3748
uses: actions/upload-artifact@v4
3849
if: success()

src/test/java/com/gitranker/api/GitRankerApiApplicationTests.java

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package com.gitranker.api.domain.user;
2+
3+
import com.gitranker.api.domain.user.vo.ActivityStatistics;
4+
import com.gitranker.api.domain.user.vo.RankInfo;
5+
import com.gitranker.api.domain.user.vo.Score;
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Nested;
8+
import org.junit.jupiter.api.Test;
9+
10+
import java.time.LocalDateTime;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class UserTest {
15+
16+
private User createDefaultUser() {
17+
return User.builder()
18+
.githubId(12345L)
19+
.nodeId("MDQ6VXNlcjEyMzQ1")
20+
.username("testuser")
21+
.email("test@example.com")
22+
.profileImage("https://avatars.githubusercontent.com/u/12345")
23+
.githubCreatedAt(LocalDateTime.of(2020, 1, 1, 0, 0))
24+
.role(Role.USER)
25+
.build();
26+
}
27+
28+
@Nested
29+
@DisplayName("생성")
30+
class Creation {
31+
32+
@Test
33+
@DisplayName("새 사용자는 0점, IRON 티어로 초기화된다")
34+
void should_initializeWithZeroScore_and_ironTier() {
35+
User user = createDefaultUser();
36+
37+
assertThat(user.getTotalScore()).isZero();
38+
assertThat(user.getTier()).isEqualTo(Tier.IRON);
39+
assertThat(user.getRanking()).isZero();
40+
}
41+
42+
@Test
43+
@DisplayName("role을 지정하지 않으면 USER로 설정된다")
44+
void should_defaultToUserRole_when_roleIsNull() {
45+
User user = User.builder()
46+
.githubId(1L)
47+
.nodeId("node1")
48+
.username("user1")
49+
.build();
50+
51+
assertThat(user.getRole()).isEqualTo(Role.USER);
52+
}
53+
54+
@Test
55+
@DisplayName("새 사용자는 isNewUser가 true를 반환한다")
56+
void should_beNewUser_when_justCreated() {
57+
User user = createDefaultUser();
58+
59+
assertThat(user.isNewUser()).isTrue();
60+
}
61+
}
62+
63+
@Nested
64+
@DisplayName("쿨다운 (5분)")
65+
class Cooldown {
66+
67+
@Test
68+
@DisplayName("생성 직후에는 쿨다운 상태이다")
69+
void should_beCooldownActive_when_justCreated() {
70+
User user = createDefaultUser();
71+
72+
// 생성 시 lastFullScanAt = now() 이므로 바로 재스캔 불가
73+
assertThat(user.canTriggerFullScan()).isFalse();
74+
}
75+
76+
@Test
77+
@DisplayName("다음 스캔 가능 시간은 마지막 스캔 시간 + 5분이다")
78+
void should_returnCorrectNextAvailableTime() {
79+
User user = createDefaultUser();
80+
LocalDateTime nextAvailable = user.getNextFullScanAvailableAt();
81+
82+
// lastFullScanAt + 5분
83+
assertThat(nextAvailable).isAfter(LocalDateTime.now().minusSeconds(1));
84+
assertThat(nextAvailable).isBefore(LocalDateTime.now().plusMinutes(6));
85+
}
86+
}
87+
88+
@Nested
89+
@DisplayName("프로필 업데이트")
90+
class ProfileUpdate {
91+
92+
@Test
93+
@DisplayName("username이 변경되면 true를 반환한다")
94+
void should_returnTrue_when_usernameChanged() {
95+
User user = createDefaultUser();
96+
97+
boolean changed = user.updateProfile("newname", user.getProfileImage(), user.getEmail());
98+
99+
assertThat(changed).isTrue();
100+
assertThat(user.getUsername()).isEqualTo("newname");
101+
}
102+
103+
@Test
104+
@DisplayName("profileImage가 변경되면 true를 반환한다")
105+
void should_returnTrue_when_profileImageChanged() {
106+
User user = createDefaultUser();
107+
108+
boolean changed = user.updateProfile(user.getUsername(), "https://new-image.png", user.getEmail());
109+
110+
assertThat(changed).isTrue();
111+
assertThat(user.getProfileImage()).isEqualTo("https://new-image.png");
112+
}
113+
114+
@Test
115+
@DisplayName("아무것도 변경되지 않으면 false를 반환한다")
116+
void should_returnFalse_when_nothingChanged() {
117+
User user = createDefaultUser();
118+
119+
boolean changed = user.updateProfile(
120+
user.getUsername(), user.getProfileImage(), user.getEmail());
121+
122+
assertThat(changed).isFalse();
123+
}
124+
125+
@Test
126+
@DisplayName("null 값은 기존 값을 유지한다")
127+
void should_keepExistingValues_when_nullPassed() {
128+
User user = createDefaultUser();
129+
String originalUsername = user.getUsername();
130+
131+
boolean changed = user.updateProfile(null, null, null);
132+
133+
assertThat(changed).isFalse();
134+
assertThat(user.getUsername()).isEqualTo(originalUsername);
135+
}
136+
}
137+
138+
@Nested
139+
@DisplayName("점수/랭킹 업데이트")
140+
class ScoreUpdate {
141+
142+
@Test
143+
@DisplayName("활동 통계로 점수와 랭킹을 동시에 업데이트한다")
144+
void should_updateScoreAndRank_when_statisticsProvided() {
145+
User user = createDefaultUser();
146+
ActivityStatistics stats = ActivityStatistics.of(100, 20, 10, 5, 15);
147+
148+
user.updateActivityStatistics(stats, 0, 100);
149+
150+
assertThat(user.getTotalScore()).isEqualTo(305);
151+
assertThat(user.getRanking()).isEqualTo(1);
152+
}
153+
154+
@Test
155+
@DisplayName("점수 업데이트 후에는 더 이상 신규 사용자가 아니다")
156+
void should_notBeNewUser_after_scoreUpdate() {
157+
User user = createDefaultUser();
158+
user.updateScore(Score.of(100));
159+
160+
assertThat(user.isNewUser()).isFalse();
161+
}
162+
}
163+
164+
@Nested
165+
@DisplayName("티어 비교")
166+
class TierComparison {
167+
168+
@Test
169+
@DisplayName("IRON 사용자는 isAtLeast(IRON)이 true이다")
170+
void should_returnTrue_when_sameOrHigherTier() {
171+
User user = createDefaultUser();
172+
173+
assertThat(user.isAtLeast(Tier.IRON)).isTrue();
174+
}
175+
176+
@Test
177+
@DisplayName("IRON 사용자는 isAtLeast(BRONZE)가 false이다")
178+
void should_returnFalse_when_lowerTier() {
179+
User user = createDefaultUser();
180+
181+
assertThat(user.isAtLeast(Tier.BRONZE)).isFalse();
182+
}
183+
184+
@Test
185+
@DisplayName("점수 업데이트 후 티어가 반영된다")
186+
void should_reflectTierAfterRankInfoUpdate() {
187+
User user = createDefaultUser();
188+
user.updateRankInfo(RankInfo.of(1, 5.0, 2000));
189+
190+
assertThat(user.getTier()).isEqualTo(Tier.MASTER);
191+
assertThat(user.isAtLeast(Tier.GOLD)).isTrue();
192+
}
193+
}
194+
195+
@Nested
196+
@DisplayName("기본값")
197+
class Defaults {
198+
199+
@Test
200+
@DisplayName("새 사용자의 getTotalScore는 0을 반환한다")
201+
void should_returnZero_when_newUser() {
202+
User user = createDefaultUser();
203+
204+
assertThat(user.getTotalScore()).isZero();
205+
assertThat(user.getRanking()).isZero();
206+
assertThat(user.getPercentile()).isEqualTo(100.0);
207+
}
208+
}
209+
}

0 commit comments

Comments
 (0)