[rest api 프로젝트 구축] - RestDocs

geun kim·2023년 3월 24일
0

REST API 구축

목록 보기
8/20

Rest Docs 목표

API 문서 만들기

○ 요청 본문 문서화
○ 응답 본문 문서화
○ 요청 헤더 문서화
○ 요청 필드 문서화
○ 응답 헤더 문서화
○ 응답 필드 문서화
○ 링크 문서화

■ self
■ query-events
■ update-event
■ profile 링크 추가

의존성 추가

<dependency>
			<groupId>org.springframework.restdocs</groupId>
			<artifactId>spring-restdocs-mockmvc</artifactId>
			<scope>test</scope>
</dependency>

템플릿 파일 추가
○ src/main/asciidoc/index.adoc << 별도 작성 해야 함 (학습 필요)

문서 빌드 : rest docs 플러그인 추가

<plugin>
				<groupId>org.asciidoctor</groupId>
				<artifactId>asciidoctor-maven-plugin</artifactId>
				<version>1.5.3</version>
				<executions>
					<execution>
						<id>generate-docs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>process-asciidoc</goal>
						</goals>
						<configuration>
							<backend>html</backend>
							<doctype>book</doctype>
						</configuration>
					</execution>
				</executions>
				<dependencies>
					<dependency>
						<groupId>org.springframework.restdocs</groupId>
						<artifactId>spring-restdocs-asciidoctor</artifactId>
						<version>2.0.2.RELEASE</version>
					</dependency>
				</dependencies>
			</plugin>
			<plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<version>2.7</version>
				<executions>
					<execution>
						<id>copy-resources</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-resources</goal>
						</goals>
						<configuration>
							<outputDirectory>
								${project.build.outputDirectory}/static/docs
							</outputDirectory>
							<resources>
								<resource>
									<directory>
										${project.build.directory}/generated-docs
									</directory>
								</resource>
							</resources>
						</configuration>
					</execution>
				</executions>
			</plugin>

RestDocMockMvc 커스터마이징 > 문서를 좀 더 예쁘게 함
● RestDocsMockMvcConfigurationCustomizer 구현한 빈 등록
Test Class에서 @Import(RestDocsConfiguration.class) 임포트 해서 사용 해야 함....

@TestConfiguration
public class RestDocsConfiguration {

    @Bean
    public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
        return configurer -> configurer.operationPreprocessors()
                .withRequestDefaults(prettyPrint())
                .withResponseDefaults(prettyPrint());
    }

}

Test DB 별도 설정
pom.xml

<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>test</scope>
		</dependency>

application-test.properties

spring.datasource.username=sa
spring.datasource.password=
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect

Test용 Resource 파일 추가 방법
test > java > resources : application-test.properties 추가

Test Resources 추가

Rest Docs 예제 : EventController.class

@PostMapping
    public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto , Errors errors){
        // 1.binding 할 때 에러가 존재 확인
        // 에러 확인 후 존재하면 에러 코드를 돌려준다.
        if(errors.hasErrors()){
            return ResponseEntity.badRequest().body(errors);
        }

        //2.입력값 검증 , eventValidator 로직 검증
        eventValidator.validate(eventDto,errors);

        //3.에러 확인 후 존재하면 에러 코드를 돌려준다.
        if(errors.hasErrors()){
            return ResponseEntity.badRequest().body(errors);
        }


        /**
         * modelmapper를 이용해서 dto의 값을 domain객체에 값 복사 해 준다.
         */
        Event event = modelMapper.map(eventDto,Event.class);
        event.bizLogic();
        Event newEvent = eventRepository.save(event);

        //hateous 제외
        //return ResponseEntity.ok().body(event); // ResponseEntity를 사용하여 header 정보 등록
      /**********************************
         * hateous 작성 -- 정말 hate 이구만...
         *********************************/
        WebMvcLinkBuilder selfLinkBuilder  = linkTo(EventContoller.class).slash(newEvent.getId());
        URI createdUri = selfLinkBuilder.toUri();
        EventResource eventResource = new EventResource(event);
        eventResource.add(linkTo(EventContoller.class).withRel("query-events"));
        eventResource.add(selfLinkBuilder.withRel("update-event"));
        eventResource.add(new Link("/docs/index.html#resources-events-create").withRel("profile"));
        return ResponseEntity.created(createdUri).body(eventResource);
    }	

Rest Docs 예제 : EventControllerTest.class

@ExtendWith(SpringExtension.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs  //restDocs 관련
@Import(RestDocsConfiguration.class) //restDocs 관련
@ActiveProfiles("test")
public class EventContollerTest {

	 @Autowired
    MockMvc mockMvc; //웹서버를 띄우지 않는다.

    @Autowired
    ObjectMapper objectMapper; //객체를 json string으로 변환

     @Test
    @TestDescription("정상적으로 동작")
    public void creatEvent() throws Exception {

        EventDto eventDto = EventDto.builder()
                .name("Spring")
                .description("REST API Development with Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2018, 11, 23, 14, 21))
                .closeEnrollmentDateTime(LocalDateTime.of(2018, 11, 24, 14, 21))
                .beginEventDateTime(LocalDateTime.of(2018, 11, 25, 14, 21))
                .endEventDateTime(LocalDateTime.of(2018, 11, 26, 14, 21))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타텁 팩토리")
                .build();

        mockMvc.perform(post("/api/events")
                        .contentType(MediaType.APPLICATION_JSON_UTF8) // 해당 요청에 JSON 본문을 보내고 있다.
                        .accept(MediaTypes.HAL_JSON) // accept-header 정의, hal_json을 받고 싶다는 선언
                        .content(objectMapper.writeValueAsString(eventDto))
                )
                .andDo(print())
                //.andExpect(status().isOk())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("id").exists())
                .andExpect(header().exists(HttpHeaders.LOCATION))
                .andExpect(header().string(HttpHeaders.CONTENT_TYPE,MediaTypes.HAL_JSON_VALUE))
                .andExpect(jsonPath("free").value(false))
                .andExpect(jsonPath("offline").value(true))
                .andExpect(jsonPath("eventStatus").value(Matchers.not(EventStatus.DRAFT)))

                /*******************************************
                 * Hateous 추가
                 ********************************************/
                /**
                .andExpect(jsonPath("_links.self").exists())
                .andExpect(jsonPath("_links.query-events").exists())
                .andExpect(jsonPath("_links.update-event").exists())
                 **/

                /*******************************************
                 * RestDocs 추가
                 ********************************************/
                .andDo(document("create-event",
                        links(
                                linkWithRel("self").description("link to self"),
                                linkWithRel("query-events").description("link to query events"),
                                linkWithRel("update-event").description("link to update an existing event"),
                                linkWithRel("profile").description("link to update an existing event")
                        ),
                        requestHeaders(
                                headerWithName(HttpHeaders.ACCEPT).description("accept header"),
                                headerWithName(HttpHeaders.CONTENT_TYPE).description("content type header")
                        ),
                        requestFields(
                                fieldWithPath("name").description("Name of new event"),
                                fieldWithPath("description").description("description of new event"),
                                fieldWithPath("beginEnrollmentDateTime").description("date time of begin of new event"),
                                fieldWithPath("closeEnrollmentDateTime").description("date time of close of new event"),
                                fieldWithPath("beginEventDateTime").description("date time of begin of new event"),
                                fieldWithPath("endEventDateTime").description("date time of end of new event"),
                                fieldWithPath("location").description("location of new event"),
                                fieldWithPath("basePrice").description("base price of new event"),
                                fieldWithPath("maxPrice").description("max price of new event"),
                                fieldWithPath("limitOfEnrollment").description("limit of enrolmment")
                        ),
                        responseHeaders(
                                headerWithName(HttpHeaders.LOCATION).description("Location header"),
                                headerWithName(HttpHeaders.CONTENT_TYPE).description("Content type")
                        ),
                        relaxedResponseFields(
                                fieldWithPath("id").description("identifier of new event"),
                                fieldWithPath("name").description("Name of new event"),
                                fieldWithPath("description").description("description of new event"),
                                fieldWithPath("beginEnrollmentDateTime").description("date time of begin of new event"),
                                fieldWithPath("closeEnrollmentDateTime").description("date time of close of new event"),
                                fieldWithPath("beginEventDateTime").description("date time of begin of new event"),
                                fieldWithPath("endEventDateTime").description("date time of end of new event"),
                                fieldWithPath("location").description("location of new event"),
                                fieldWithPath("basePrice").description("base price of new event"),
                                fieldWithPath("maxPrice").description("max price of new event"),
                                fieldWithPath("limitOfEnrollment").description("limit of enrolmment"),
                                fieldWithPath("free").description("it tells if this event is free or not"),
                                fieldWithPath("offline").description("it tells if this event is offline event or not"),
                                fieldWithPath("eventStatus").description("event status"),
                                fieldWithPath("_links.self.href").description("link to self"),
                                fieldWithPath("_links.query-events.href").description("link to query event list"),
                                fieldWithPath("_links.update-event.href").description("link to update existing event"),
                                fieldWithPath("_links.profile.href").description("link to profile")
                        )
                ));

    }
 }

문서 생성하기

● 템플릿 파일 추가 : src/main/asciidoc/index.adoc << 템플릿 파일도 작성 해야 함....
● mvn package
● 문서 조각 위치 : C:\myProject\jinyoung_rest\target\generated-snippets
● 문서 확인 C:\myProject\jinyoung_rest\target\classes\static\docs\index.html
● 웹으로 문서 보기 : http://localhost:8080/docs/index.html

profile
Devops Load Map

0개의 댓글