개발자로 후회없는 삶 살기
[테스트] Spring Rest Docs 셋팅 (ClassNotFoundException 에러 해결) 본문
[테스트] Spring Rest Docs 셋팅 (ClassNotFoundException 에러 해결)
몽이장쥰 2025. 1. 14. 16:01서론
※ 아래 내용을 다룹니다.
- 스프링 Rest Docs 셋팅
- ClassNotFound 예외 해결
본론
- Rest Docs 사용 목적
API 명세 작성 > 스펙 작성 > 문서 작성
프론트엔드 개발자와 프로젝트를 진행할 때, API 명세를 작성하고 문서화하여 공유하는 것이 일반적이다. 이때 Rest Docs을 사용하면 개발이 완료되기 전에, 간단한 컨트롤러 테스트 코드로 API를 문서화할 수 있다. 또한 테스트가 성공해야만 문서가 만들어지므로, 테스트 코드 작성을 의무적으로 수행하기 위한 팀 규율로 사용할 수도 있다.
✅ 도메인 > 서비스 > 컨트롤러 순으로 개발을 하면 문서화 시점이 늦지 않을까?
실무에서는 API 명세에 맞게 컨트롤러의 요청과 응답, 빈 검증만 간단하게 개발하여 Rest Docs로 문서화하고, 도메인과 서비스를 진행하기도 한다.
-> 특징
1) 테스트 코드를 사용하여 문서를 만든다.
2) AsciiDoc 문법을 사용하여 문서를 작성한다.
-> 장점
1) 프로덕션 코드에 비침투적이라서 안전하다. 프로덕션 코드를 건드리지 않고 테스트 코드만 작성해서 문서를 만들 수 있다.
2) 테스트 코드가 성공해야 문서를 만들 수 있어서, 문서가 만들어졌다는 것은 실제로 그 문서대로 동작하고 있음을 보장한다.
-> 단점
코드가 길고, 설정이 어렵다.
- gradle 설정
1. asciidoctor plugins 설치
문서 조각을 HTML로 만들어주는 플러그인 asciidoctor
2. dependencies
// RestDocs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
3. 전역변수 설정
ext { // 전역 변수
snippetsDir = file('build/generated-snippets')
}
문서화할 문서 조각을 스니펫이라고 한다. 스니펫이 저장될 경로를 전역으로 지정한다.
4. asciidoctor 시나리오
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
sources { // adoc이 여러개로 분리되어서 include할때, 특정 adoc 파일만 html로 만든다.
include("**/index.adoc")
}
baseDirFollowsSourceFile() // 다른 adoc 파일을 include 할 때 경로를 baseDir로 맞춘다.
dependsOn test
}
bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
테스트를 실행하면 스니펫이 스니펫 dir에 생성된다. asciidoctor로 문서 조각을 사용하여 html을 생성하고 jar에 그 문서를 담아서 배포한다.
5. asciidoc 플러그인 설치
인텔리제이 플러그인은 작성한 adoc 파일을 한눈에 보기 쉽게 만들어주니 설치하자.
- 테스트 코드 작성
@ExtendWith(RestDocumentationExtension.class)
public abstract class RestDocsSupport {
Rest Docs를 사용하려면 익스텐션이 필요하다.
@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.standaloneSetup(initController())
.apply(documentationConfiguration(provider))
.build();
}
@SpringBootTest를 사용하면, 테스트를 돌릴 때마다 스프링부트가 뜨고, 이는 테스트 비용이 커지는 것으로 이어지는 문제점이 있다. 문서를 하나 만드는 데 스프링 부트를 하나 띄우는 것은 올바르지 않다고 판단되어 스프링부트 없이 RestDocs를 사용하는 컨트롤러 단위 테스트를 설정하자. 스프링과 무관한 컨트롤러 단위 테스트라고 볼 수 있다.
https://hsb422.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-Mock
테스트 비용은 위 블로그에서 확인할 수 있다.
protected abstract Object initController();
standalone으로 스프링부트 없이 단위테스트를 할 땐, 테스트할 컨트롤러 타입을 지정해줘야 한다. 하지만 모든 테스트마다 지정하는 것은 번거로우므로, 추상 메서드를 만들고 하위 클래스에서 넣는 방식으로 한다.
@BeforeEach
void setUp(WebApplicationContext context, RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.build();
}
스프링 부트를 사용할 땐 WebApp 컨텍스트를 주입받아서 사용하면 된다.
-> 테스트 작성
mockMvc.perform(
post("/api/v1/products/new")
.content(objectMapper.writeValueAsString(request))
.contentType(APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isOk())
.andDo(MockMvcRestDocumentation.document( // 하위에 작성
문서를 만들기 위한 체이닝을 넣어주면 된다. andDo의 document에 RestDocs 문서에 넣을 내용을 추출해야 한다.
✅ 어떤 것을 정의를 해야 할까?
Rest Docs는 API 문서라고 했다. 따라서 스니펫은 요청(쿼리 파라미터 등)과 응답에 대한 정의를 한다.
requestFields(
fieldWithPath("type").type(JsonFieldType.STRING)
.description("상품 타입"),
fieldWithPath("sellingStatus").type(JsonFieldType.STRING)
.optional()
.description("상품 판매상태"),
fieldWithPath("name").type(JsonFieldType.STRING)
.description("상품 이름"),
fieldWithPath("price").type(JsonFieldType.NUMBER)
.description("상품 가격")
),
responseFields(
fieldWithPath("code").type(JsonFieldType.NUMBER)
.description("코드"),
fieldWithPath("httpStatus").type(JsonFieldType.STRING)
.description("상태"),
fieldWithPath("message").type(JsonFieldType.STRING)
.description("메시지"),
fieldWithPath("data").type(JsonFieldType.OBJECT)
.description("데이터"),
fieldWithPath("data.id").type(JsonFieldType.NUMBER)
.description("상품 ID"),
fieldWithPath("data.productNumber").type(JsonFieldType.STRING)
.description("상품 번호"),
fieldWithPath("data.type").type(JsonFieldType.STRING)
.description("상품 타입"),
fieldWithPath("data.sellingStatus").type(JsonFieldType.STRING)
.description("상품 판매상태"),
fieldWithPath("data.name").type(JsonFieldType.STRING)
.description("상품 이름"),
fieldWithPath("data.price").type(JsonFieldType.NUMBER)
.description("상품 가격")
)
요청은 request dto의 필드의 타입, 그에 대한 설명을 모든 필드에 대해 넣어준다. response도 동일하다.
테스트를 실행하거나 gradle의 asciidoctor를 실행하면 andDo의 키 이름으로 폴더가 생기고, 문서 조각이 생성된다.
-> index.adoc으로 문서 만들기
gradle에 정의한 대로 index.adoc를 사용하여 html을 만들기 위한 내용을 작성한다. 상위 if 문은 생성된 문서 조각의 경로를 지정하는데, index.adoc 파일에 문서 조각을 가져와서 html을 만들 것이다.
src/docs/asciidoc에 index.adoc 파일을 만들고 asciidoctor를 실행하면 build/docs/asciidoc/index.html이 생성된다.
-> 전/후처리
rest docs의 특징은 커스터마이징이다.
1. JSON Pretty
preprocessRequest(prettyPrint())
preprocessResponse(prettyPrint())
json pretty를 지정할 수 있다.
2. Optional 지정
테스트 코드의 Optional을 원하는 field에 설정하면
컬럼에 추가할 수 있다.
3. index.adoc 분리
index.adoc을 통해 html을 만들게 되는데, API가 많아질수록 adoc도 길어져서, 한눈에 보기 어렵고 유지보수 및 수정이 어려워진다. 따라서 여러 개의 adoc으로 분리하고 build 할 때 하나의 index.html로 모을 수 있다.
✅ 문서를 공유하는 방법
bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
이렇게 만들어진 문서를 팀원에게 공유하기 위해선 jar 파일 내부에 포함시켜서 localhost:8080/docs/index.html로 공유할 수 있다. 이것은 팀원들끼리 룰로 정하면 될 것이다.
🚨 ClassNotFound Test Build Fail 오류 해결
asciidoc build는 반드시 프로젝트 경로에 한글이 있으면 안 된다. 평소에도 바탕하면 하위에 스프링 프로젝트를 설치하여 테스트를 진행해 와서 한글 문제는 아닐 거라고 생각했지만, 스프링 프로젝트 전체 경로에서 한글을 제거하니 asciidoc build를 성공했다.
'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글
[테스트] 컨트롤러 테스트 하는 방법 (with Mockito) (0) | 2025.01.08 |
---|---|
[문법] 스프링 트랜잭션 전파 활용 (0) | 2024.08.08 |
[문법] 스프링 예외 추상화 (0) | 2024.08.07 |
[문법] 스프링 트랜잭션 이해와 적용 (0) | 2024.08.06 |
[문법] 파일 업로드 (0) | 2024.08.05 |