Skip to content

Commit 450db99

Browse files
authored
feat: test skill 추가 (#647)
* feat: serena MCP 추가 * feat: test skill 추가 * feat: hook 추가 - 응답 대기시 알람발송 - 컨벤션 어겼을 때 훅 작동 * feat: 안쓰는 파일 제거
1 parent 2e84c2f commit 450db99

File tree

7 files changed

+444
-241
lines changed

7 files changed

+444
-241
lines changed

.claude/hooks/post-edit-check.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import sys
4+
import re
5+
6+
data = json.load(sys.stdin)
7+
file_path = data.get("tool_input", {}).get("file_path", "")
8+
9+
if not file_path.endswith(".java") or not file_path:
10+
sys.exit(0)
11+
12+
try:
13+
with open(file_path) as f:
14+
content = f.read()
15+
lines = content.split("\n")
16+
except Exception:
17+
sys.exit(0)
18+
19+
warnings = []
20+
21+
# 1. 와일드카드 import 체크
22+
for i, line in enumerate(lines, 1):
23+
if re.match(r"\s*import\s+.*\.\*;", line):
24+
warnings.append(f"L{i}: 와일드카드 import 발견 -> 명시적 import 필요")
25+
26+
# 2. 파일 끝 줄바꿈 체크
27+
if content and not content.endswith("\n"):
28+
warnings.append("파일 끝 줄바꿈 누락")
29+
30+
# 3. Entity 클래스의 @Column 체크
31+
if "@Entity" in content:
32+
field_pattern = re.compile(r"^\s+private\s+\w+(?:<[^>]+>)?\s+\w+;")
33+
relation_annotations = {
34+
"@Column", "@Id", "@ManyToOne", "@OneToMany",
35+
"@JoinColumn", "@OneToOne", "@ManyToMany",
36+
"@Transient", "@Version", "@Embedded", "@EmbeddedId",
37+
}
38+
for i, line in enumerate(lines):
39+
if field_pattern.match(line):
40+
preceding = "\n".join(lines[max(0, i - 5):i])
41+
has_annotation = any(ann in preceding for ann in relation_annotations)
42+
if not has_annotation:
43+
warnings.append(f"L{i + 1}: Entity 필드에 @Column 누락 가능성: {line.strip()}")
44+
45+
if warnings:
46+
print(f"[컨벤션 체크 - {file_path.split('/')[-1]}]")
47+
for w in warnings:
48+
print(f" - {w}")

.claude/settings.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"env": {
3+
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
4+
},
5+
"hooks": {
6+
"Notification": [
7+
{
8+
"matcher": "",
9+
"hooks": [
10+
{
11+
"type": "command",
12+
"command": "osascript -e 'display notification \"Awaiting your input\" with title \"Claude Code\"'"
13+
}
14+
]
15+
}
16+
],
17+
"PostToolUse": [
18+
{
19+
"matcher": "Edit|Write",
20+
"hooks": [
21+
{
22+
"type": "command",
23+
"command": "python3 .claude/hooks/post-edit-check.py"
24+
}
25+
]
26+
}
27+
]
28+
}
29+
}

.claude/skills/test/SKILL.md

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
name: test
3+
description: 테스트 코드를 작성하거나 수정할 때 이 프로젝트의 테스트 컨벤션과 패턴을 참고합니다
4+
---
5+
6+
# 테스트 코드 작성 가이드
7+
8+
## 테스트 기본 설정
9+
10+
모든 통합 테스트는 `@TestContainerSpringBootTest` 어노테이션을 사용합니다.
11+
12+
```java
13+
@TestContainerSpringBootTest
14+
@DisplayName("채팅 서비스 테스트")
15+
class ChatServiceTest {
16+
// 테스트 코드
17+
}
18+
```
19+
20+
**제공 기능:**
21+
- MySQL, Redis 자동 실행
22+
- Spring Boot 컨텍스트 로드
23+
- 테스트 후 자동 DB 초기화
24+
- JUnit 5 기반
25+
26+
## Fixture 패턴
27+
28+
테스트 데이터는 Fixture로 생성합니다 (FixtureBuilder + Fixture 패턴).
29+
30+
**위치:** `src/test/java/com/example/solidconnection/[domain]/fixture/`
31+
32+
```
33+
fixture/
34+
├── [Entity]FixtureBuilder.java # Builder 패턴 구현
35+
└── [Entity]Fixture.java # 편의 메서드 제공
36+
```
37+
38+
### 예제: ChatRoomFixtureBuilder
39+
40+
```java
41+
@TestComponent
42+
@RequiredArgsConstructor
43+
public class ChatRoomFixtureBuilder {
44+
45+
private final ChatRoomRepository chatRoomRepository;
46+
47+
private boolean isGroup;
48+
private Long mentoringId;
49+
50+
public ChatRoomFixtureBuilder chatRoom() {
51+
return new ChatRoomFixtureBuilder(chatRoomRepository);
52+
}
53+
54+
public ChatRoomFixtureBuilder isGroup(boolean isGroup) {
55+
this.isGroup = isGroup;
56+
return this;
57+
}
58+
59+
public ChatRoomFixtureBuilder mentoringId(long mentoringId) {
60+
this.mentoringId = mentoringId;
61+
return this;
62+
}
63+
64+
public ChatRoom create() {
65+
ChatRoom chatRoom = new ChatRoom(mentoringId, isGroup);
66+
return chatRoomRepository.save(chatRoom); // DB 저장
67+
}
68+
}
69+
```
70+
71+
### 예제: ChatRoomFixture
72+
73+
```java
74+
@TestComponent
75+
@RequiredArgsConstructor
76+
public class ChatRoomFixture {
77+
78+
private final ChatRoomFixtureBuilder chatRoomFixtureBuilder;
79+
80+
// 편의 메서드: 기본값으로 생성
81+
public ChatRoom 채팅방(boolean isGroup) {
82+
return chatRoomFixtureBuilder.chatRoom()
83+
.isGroup(isGroup)
84+
.create();
85+
}
86+
87+
public ChatRoom 멘토링_채팅방(long mentoringId) {
88+
return chatRoomFixtureBuilder.chatRoom()
89+
.mentoringId(mentoringId)
90+
.isGroup(false)
91+
.create();
92+
}
93+
}
94+
```
95+
96+
**편의 메서드 작성 팁:**
97+
98+
- 한국어 메서드명 사용 (가독성)
99+
- 자주 사용되는 기본값 조합만 제공
100+
- Builder를 조합하여 필요한 데이터 설정
101+
102+
### 테스트에서 사용
103+
104+
```java
105+
@TestContainerSpringBootTest
106+
class ChatServiceTest {
107+
108+
@Autowired
109+
private ChatRoomFixture chatRoomFixture;
110+
111+
@Test
112+
void 채팅방을_생성할_수_있다() {
113+
// 편의 메서드 사용
114+
ChatRoom room = chatRoomFixture.채팅방(false);
115+
116+
// Builder 직접 사용
117+
ChatRoom customRoom = chatRoomFixture.chatRoomFixtureBuilder.chatRoom()
118+
.isGroup(true)
119+
.mentoringId(100L)
120+
.create();
121+
}
122+
}
123+
```
124+
125+
## 테스트 네이밍 컨벤션
126+
127+
### 테스트 메서드 네이밍 규칙
128+
129+
테스트 메서드명은 **한국어로 명확하게** 작성하며, 다음 패턴을 따릅니다:
130+
131+
#### 1. 정상 동작 테스트
132+
133+
```java
134+
// 패턴: 어떤_것을_하면_어떤_결과가_나온다
135+
@Test
136+
void 채팅방이_없으면_빈_목록을_반환한다() { ... }
137+
138+
@Test
139+
void 최신_메시지_순으로_정렬되어_조회한다() { ... }
140+
141+
@Test
142+
void 참여자는_메시지를_전송할_수_있다() { ... }
143+
144+
@Test
145+
void 페이징이_정상_작동한다() { ... }
146+
```
147+
148+
#### 2. 예외 테스트
149+
150+
```java
151+
// 패턴: 어떤_것을_하면_예외_응답을_반환한다
152+
@Test
153+
void 참여하지_않은_채팅방에_접근하면_예외_응답을_반환한다() { ... }
154+
155+
@Test
156+
void 존재하지_않는_사용자로_메시지를_전송하면_예외_응답을_반환한다() { ... }
157+
158+
@Test
159+
void 권한이_없으면_예외_응답을_반환한다() { ... }
160+
161+
@Test
162+
void 필수_파라미터가_없으면_예외_응답을_반환한다() { ... }
163+
```
164+
165+
## BDD 테스트 작성
166+
167+
테스트는 Given-When-Then 구조로 작성합니다.
168+
169+
```java
170+
@Test
171+
@DisplayName("최신 메시지순으로 채팅방 목록을 조회한다")
172+
void 최신_메시지_순으로_조회한다() {
173+
// Given: 테스트 사전 조건
174+
SiteUser user = siteUserFixture.사용자();
175+
ChatRoom room1 = chatRoomFixture.채팅방(false);
176+
ChatRoom room2 = chatRoomFixture.채팅방(false);
177+
chatMessageFixture.메시지("오래된 메시지", user.getId(), room1);
178+
chatMessageFixture.메시지("최신 메시지", user.getId(), room2);
179+
180+
// When: 실제 동작
181+
ChatRoomListResponse response = chatService.getChatRooms(user.getId());
182+
183+
// Then: 결과 검증
184+
assertAll(
185+
() -> assertThat(response.chatRooms()).hasSize(2),
186+
() -> assertThat(response.chatRooms().get(0).id()).isEqualTo(room2.getId())
187+
);
188+
}
189+
```
190+
191+
## 테스트 그룹화 (@Nested)
192+
193+
기능별로 테스트를 그룹화합니다.
194+
195+
```java
196+
@TestContainerSpringBootTest
197+
class ChatServiceTest {
198+
199+
@Nested
200+
@DisplayName("채팅방 목록 조회")
201+
class 채팅방_목록을_조회한다 {
202+
203+
@Test
204+
void 빈_목록을_반환한다() { ... }
205+
206+
@Test
207+
void 최신_메시지_순으로_조회한다() { ... }
208+
}
209+
210+
@Nested
211+
@DisplayName("채팅 메시지 전송")
212+
class 채팅_메시지를_전송한다 {
213+
214+
@BeforeEach
215+
void setUp() {
216+
// 이 그룹에만 적용되는 초기 설정
217+
}
218+
219+
@Test
220+
void 참여자는_메시지를_전송할_수_있다() { ... }
221+
}
222+
}
223+
```
224+
225+
## 자주 사용하는 Assertion
226+
227+
```java
228+
// 기본 검증
229+
assertThat(value).isEqualTo(expected);
230+
assertThat(value).isNotNull();
231+
232+
// 컬렉션
233+
assertThat(list).hasSize(3);
234+
assertThat(list).isEmpty();
235+
assertThat(list).contains(item);
236+
237+
// 예외 검증
238+
assertThatCode(() -> method())
239+
.isInstanceOf(CustomException.class)
240+
.hasMessage("error message");
241+
242+
// 복수 검증
243+
assertAll(
244+
() -> assertThat(a).isEqualTo(1),
245+
() -> assertThat(b).isEqualTo(2)
246+
);
247+
```

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ out/
3737
### VS Code ###
3838
.vscode/
3939

40+
### Claude Code ###
41+
.claude/settings.local.json
42+
4043
### YML ###
4144
application-secret.yml
4245
application-prod.yml

.serena/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/cache

0 commit comments

Comments
 (0)