문서 클래스 표 계약 테스트를 되살린다
architecture.md의 클래스 책임 표는 코드 구조를 문서에 복제한 것이라 rename이나 package 이동이 있으면 조용히 낡는다. 이 테스트는 그 이유로 만들어졌고, 삭제된 원본 Javadoc에 "패키지 재구성 한 번에 네 개의 이름이 죽은 적이 있다"고 적혀 있다. c9a6bd2의 dap에서 dat으로의 개명에서 같은 일이 다시 일어났다. 테스트가 그 커밋에서 함께 삭제되어 드리프트를 잡지 못했고, dc6f602에서 사람이 손으로 대조해 아홉 건을 고쳐야 했다. 패키지 문자열만 dat으로 바꿔 복원한다. 표의 클래스 행 서른 개를 모두 읽어 src/main/java에 해당 소스가 있는지 확인한다. 패키지 경계 표의 일곱 행은 첫 칸이 소문자 패키지명이라 정규식이 의도대로 건너뛰므로 사각지대는 없다. 존재하지 않는 클래스 행을 표에 심어 실패를 내는 것까지 확인했고 확인 후 되돌렸다. 195개 테스트 전부 통과. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package io.shinhanlife.dat.biz.mcp.docs;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* {@code docs/architecture.md}의 클래스 책임 표가 실제 소스와 어긋나지 않는지 확인하는 문서 계약 테스트입니다. 이 표는 코드 구조를 문서에 복제한 것이라 class를 rename하거나 package를 옮기면 조용히 낡습니다. 실제로 패키지 재구성 한 번에 네
|
||||
* 개의 이름이 죽은 적이 있어, 사람의 주의력 대신 테스트로 고정합니다. 소스를 읽기만 하며 애플리케이션 context를 띄우지 않습니다.
|
||||
*/
|
||||
class ArchitectureDocumentContractTest {
|
||||
|
||||
private static final Path ARCHITECTURE = Path.of("docs", "architecture.md");
|
||||
private static final Path MAIN_PACKAGE =
|
||||
Path.of("src", "main", "java", "io", "shinhanlife", "dat", "biz", "mcp");
|
||||
/**
|
||||
* 표의 첫 두 칸에 백틱으로 감싼 타입 이름과 패키지 경로가 있는 행만 뽑는다.
|
||||
*/
|
||||
private static final Pattern TABLE_ROW =
|
||||
Pattern.compile("^\\| `([A-Z][A-Za-z0-9]*)` \\| `([a-z0-9/]+)` \\|");
|
||||
|
||||
/**
|
||||
* 클래스 표에 적힌 모든 타입이 {@code src/main/java}에 실제로 존재하는지 확인합니다. 존재하지 않는 이름이 있으면 rename 후 문서를 갱신하지 않은 것이므로, 어떤 이름인지 함께 알려 줍니다.
|
||||
*/
|
||||
@Test
|
||||
void everyDocumentedClassPathStillExists() throws IOException {
|
||||
List<DocumentedType> documented = documentedTypes();
|
||||
|
||||
// 표 자체가 사라지면 이 테스트가 조용히 통과해 버리므로 최소 개수를 함께 고정한다.
|
||||
assertThat(documented)
|
||||
.withFailMessage("architecture.md의 클래스 책임 표를 찾지 못했습니다. 표 형식이 바뀌었는지 확인하세요.")
|
||||
.hasSizeGreaterThan(10);
|
||||
|
||||
List<DocumentedType> missing = documented.stream().filter(type -> !sourceExists(type)).toList();
|
||||
|
||||
assertThat(missing)
|
||||
.withFailMessage(
|
||||
"architecture.md에 적힌 package와 class 경로에 소스가 없는 타입: %s%n"
|
||||
+ "class를 rename하거나 package를 옮겼다면 문서의 표도 같은 변경에서 고쳐야 합니다.",
|
||||
missing)
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 클래스 책임 표에서 타입 이름과 패키지 경로를 순서대로 모읍니다.
|
||||
*/
|
||||
private List<DocumentedType> documentedTypes() throws IOException {
|
||||
try (Stream<String> lines = Files.lines(ARCHITECTURE)) {
|
||||
return lines.map(TABLE_ROW::matcher)
|
||||
.filter(Matcher::find)
|
||||
.map(matcher -> new DocumentedType(matcher.group(1), matcher.group(2)))
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 문서에 적힌 패키지와 타입 이름이 가리키는 main 소스 파일이 정확히 존재하는지 확인합니다.
|
||||
*/
|
||||
private boolean sourceExists(DocumentedType type) {
|
||||
return Files.isRegularFile(MAIN_PACKAGE.resolve(type.packagePath()).resolve(type.name() + ".java"));
|
||||
}
|
||||
|
||||
private record DocumentedType(String name, String packagePath) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user