팀원들과 Rest Docs와 Swagger 중 무엇을 고민할지 상의했다.
결과적으로는 나의 생각과는 다르게 Rest Docs를 선택했다.
Swagger도 튜토리얼 정도만 해봤기 때문에 둘의 장단점을 명확하게 공감하지는 못한다.
그래서 이번에는 팀원들의 의견을 따라 Rest Docs를 사용하며, 장단점을 느껴보자.
하지만 팀원 분은 프로덕션 코드에 의존되는 것 자체가 가독성이 좋지 않다고 했고,
@ApiModel을 사용해서 추상화를 한다고 해도 결국 추상화를 위한 클래스가 필요하기 때문에 별도로 클래스를 관리하는 것이
개발할 때 찾아가기 불편하다고 했다. 하지만 생각해보니 이 부분은 Rest Docs도 똑같은 것 같다.
결국 Rest Docs도 깔끔하게 하려면 추상화가 필요하다. 그래서 아직까지는 Swagger가 좋지 않을까 생각하고 있다.
우리 프로젝트는 문서화 도구로 Markdown
이 아닌 AsciiDoc
을 사용했다.
ATDD 교육 과정에서 사용했었는데 이유는 include
의 여부이다.
Markdown
은 문법이 굉장이 편하다. 그리고 많이 사용하는 문법이다.
반면 AsciiDoc
은 문법은 조금 불편하지만 include가 가능
하기 때문에 html을 작성하는 것처럼
재활용이 가능하다. 그래서 AsciiDoc
을 채택하게 되었다.
테스트에서 공통으로 사용될 셋팅을 만든다.
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.request.ParameterDescriptor;
import org.springframework.test.context.ActiveProfiles;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document;
import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration;
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
public class Documentation {
@LocalServerPort
int port;
protected RequestSpecification spec;
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
RestAssured.port = port;
this.spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(restDocumentation))
.build();
}
RequestSpecification given(String identifier, ParameterDescriptor[] parameterDescriptors, FieldDescriptor[] fieldDescriptors) {
return RestAssured.given(this.spec).log().all()
.filter(document(identifier, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
requestParameters(parameterDescriptors),
relaxedResponseFields(fieldDescriptors)
)
);
}
}
protected RequestSpecification spec
Rest Assured
를 사용하기 때문에 사용한다.Rest Assured
의 given()
메서드의 리턴 타입이 RequestSpecification
이다.RestDocumentationContextProvider
, RestDocumentationExtension.class
RestDocumentationContextProvider
의 구현체로 JUnitRestDocumentation
을 제공한다.@BeforeEach
에서 주입이 필요하다.document
@ExtendWith(RestDocumentationExtension.class)
rest docs를 확장하여 조금 더 편리하게 사용하기 위해 테스트에서 공통으로 사용할 기능을 지원한다.
mockito로 예를 들어보자면 아래와 같다.
공통으로 사용하도록 @Mock을 사용하여 공통화할 수 있다.
@ExtendWith(MockitoExtension.class)
public class MockitoExtensionTest {
@Mock
private LineRepository lineRepository;
@Mock
private StationService stationService;
@Test
void findAllLines() {
// given
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
LineService lineService = new LineService(lineRepository, stationService);
...
}
공통화를 못하고 각 테스트마다 사용해야 한다.
@DisplayName("단위 테스트 - mockito를 활용한 가짜 협력 객체 사용")
public class MockitoTest {
@Test
void findAllLines() {
// given
LineRepository lineRepository = mock(LineRepository.class);
StationService stationService = mock(StationService.class);
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
LineService lineService = new LineService(lineRepository, stationService);
...
}
build.gradle 셋팅
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "org.asciidoctor.jvm.convert" version "3.3.2"
id 'java'
}
configurations {
asciidoctorExt
}
group = 'project'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// handlebars
implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0'
// rest docs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.5.RELEASE'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured:2.0.5.RELEASE'
testImplementation 'io.rest-assured:rest-assured:3.3.0'
}
// snippets 파일이 저장될 경로 변수 설정
ext {
snippetsDir = file('build/generated-snippets')
}
// 테스트 실행 시 snippets를 저장할 경로로 snippetsDir 사용
test {
useJUnitPlatform()
}
task testDocument(type: Test) {
useJUnitPlatform()
filter {
includeTestsMatching "*.documentation.*"
}
}
// API 문서 생성
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn testDocument
}
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
task
: gradle
을 통해 실행되는 단위
ext
: 전역 변수 셋팅
asciidoctor
: testDocument
를 의존하여 테스트를 실행하고, snippetsDir
에서 snippets
을 참조하여
문서를 생성한다.
bootJar
: jar
빌드 시 asciidoctor
를 참조하여 문서를 생성하고, snippetsDir
에 있는 html5
파일을
static/docs
로 복사한다. 복사하는 이유는 api 요청으로 문서 접근을 위함
copyDocument
: from
디렉토리에 있는 API 문서 파일을 into
로 복사한다.
복사하는 이유는 api 요청으로 문서 접근을 위함. 테스트 시에는 이걸 쓰고, 배포 시에는 bootjar
를 씀.
testDocument
: restdocs
전용 테스트 실행 task로 **.documnetation.*
에 해당하는 테스트만 실행한다.*
includeTestMatching
: 실행할 테스트 패턴 정의
- 우리는 Documentation
이라는 RestDocs 전용 부모 클래스를 상속받아 사용하기 때문에
**.documnetation.*
로 지정*
- https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/TestFilter.html
테스트(testDocument
)를 수행시켜 snippet
을 생성한다.
build.gradle
에서 설정한 snippetsDir
에 생성 됨.gradle로 asciidoctor task
를 수행시켜 문서 파일을 생성
src/docs/asciidoc/index.adoc
있어야 함.asciidoctor
가 testDocument
를 의존하기 때문에 asciidoctor
를 바로 실행해도 됨.build > asciidoc > html5 > index.html
에 문서가 생성된 것을 오픈하여 잘 만들어졌는지 확인한다.
task copyDocument를 실행하여 배포할 디렉토리도 복사하기