[Spring REST API #4] Spring REST API 테스트 클래스 생성 및 201 응답 받기

2021. 3. 26. 02:24 Spring Data/Spring Data REST

 

| Spring REST API  테스트 클래스 생성 및 201 응답 받기

 

모든 소스 코드는 여기에서 보실 수 있습니다.

 

 

프로젝트 구조

+---src
|   +---main
|   |   +---java
|   |   |   \---com
|   |   |       \---example
|   |   |           \---springrestapi
|   |   |               |   SpringRestApiApplication.java
|   |   |               |
|   |   |               \---events
|   |   |                       Event.java
|   |   |                       EventController.java
|   |   |                       EventRepository.java
|   |   |                       EventStatus.java
|   |   |
|   |   \---resources
|   |       |   application.properties
|   |       |
|   |       +---static
|   |       \---templates
|   \---test
|       \---java
|           \---com
|               \---example
|                   \---springrestapi
|                       |   SpringRestApiApplicationTests.java
|                       |
|                       \---events
|                               EventControllerTests.java
|                               EventTest.java

 

의존성 관리

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-hateoas</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.modelmapper</groupId>
		<artifactId>modelmapper</artifactId>
		<version>2.3.1</version>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>com.h2database</groupId>
		<artifactId>h2</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.restdocs</groupId>
		<artifactId>spring-restdocs-mockmvc</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

 

테스트 코드

package com.example.springrestapi.events;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import java.time.LocalDateTime;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest // Register all beans to test. We can use @WebMvcTest to test as slice test. But this way is easy.
@AutoConfigureMockMvc
public class EventControllerTests {

    @Autowired // Enable to test cases because web server doesn't work
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void createEvent() throws Exception {
        Event event = Event.builder()
                .name("Spring")
                .description("REST API Development")
                .beginEnrollmentDateTime(LocalDateTime.of(2010, 11, 23, 14, 23))
                .closeEnrollmentDateTime(LocalDateTime.of(2018, 11, 30, 14, 23))
                .beginEventDateTime(LocalDateTime.of(2018, 12, 5, 14, 30))
                .endEventDateTime(LocalDateTime.of(2018, 12, 6, 14, 30))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("D Start up Factory")
                .build();

        mockMvc.perform(post("/api/events/")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .accept(MediaTypes.HAL_JSON_UTF8)
                        .content(objectMapper.writeValueAsString(event)))
                    .andDo(print())
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("id").exists())
                    .andExpect(header().exists(HttpHeaders.LOCATION))
                    .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE));
    }

}

 

  • 이벤트 빌더를 써서 이벤트 객체를 생성하여 테스트하는 코드다.
  • 위에서 생성한 이벤트 객체를 /api/events/ 경로로 POST 메서드를 통해 이벤트 데이터를 생성하는 로직을 검사하는 테스트다. 
  • 실제 웹 컨테이너를 구동하지 않고 MockMvc를 써서 테스트를 진행할 수 있다. 웹 컨테이너가 구동되지 않는만큼 테스트를 더 빨리 돌릴 수 있다.
  • ObjectMapper를 의존성으로 org.modelmapper를 추가함으로써 objectMapper 레퍼런스에 잭슨의 ObjectMapper 객체가 자동적으로 주입된다. 
  • objectMapper.writeValueAsString 메서드를 통해 객체를 JSON으로 직렬화하여 HTTP 메세지에 추가한다. 
  • andDo(print())를 사용하면 테스트 콘솔창에 해당 Request 및 Response를 볼 수 있어 디버깅하기 편하다.
  • andExpect 메서드를 통해서 어떤 응답이 올지 체크하는 로직을 쉽게 작성할 수 있다. 
  • HttpHeaders에 등록된 상수들을 통해서 type-safe한 코드를 작성할 수 있다.

 

소스 코드

package com.example.springrestapi.events;

import lombok.*;

import javax.persistence.*;
import java.time.LocalDateTime;


/**
 * In case of referencing between entities, using default @EqualsAndHashCode stack overflow.
 * Therefore, redefine the way of checking equality is the best practice as below.
 * Also, you shouldn't use @Data annotation because it uses default @EqualsAndHashCode
 */
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = "id")
public class Event {

    @Id @GeneratedValue
    private Integer id;
    private String name;
    private String description;
    private LocalDateTime beginEnrollmentDateTime;
    private LocalDateTime closeEnrollmentDateTime;
    private LocalDateTime beginEventDateTime;
    private LocalDateTime endEventDateTime;
    private String location; // (optional)
    private int basePrice; // (optional)
    private int maxPrice;  // (optional)
    private int limitOfEnrollment;
    private boolean offline;
    private boolean free;
    @Enumerated(EnumType.STRING)
    private EventStatus eventStatus = EventStatus.DRAFT;

}

 

  • 이벤트 엔티티의 소스코드다. @Entity 어노테이션으로 이 클래스가 엔티티를 나타내는 것을 나타낸다. 또한 @Id, @GeneratedValue를 통해 자동적으로 ID가 생성될 수 있게 한다.
  • @Enumerated(EnumType.STRING)을 통해 현재 Enum값이 문자열로서 나타나도록 한다. default값인 ORDINAL로 할 경우 EnumType의 순서가 바뀔 시 큰 에러를 낼 수 있으므로 문자열로서 나타내게 하는 것이 바람직 하다.

package com.example.springrestapi.events;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import java.net.URI;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;

@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {

    @Autowired
    EventRepository eventRepository;

    @PostMapping
    public ResponseEntity createEvent(@RequestBody Event event) {
        Event newEvent = this.eventRepository.save(event);
        URI createdURI = linkTo(EventController.class).slash(newEvent.getId()).toUri();
        return ResponseEntity.created(createdURI).body(newEvent);
    }
}
  • HTTP 요청을 처리하는 컨트롤러 소스코드다. @RequestMapping 어노테이션을 통해 이 컨트롤러가 /api/events URL과 매핑된 요청을 처리하는 것을 나타낸다. 또한 HAL_JSON_UTF8_VALUE 형태로서 값을 반환하는 것을 볼 수 있다.
  • EventRepository 인터페이스에 자동적으로 객체가 주입된다. 이 eventRepository 인터페이스를 통해 객체를 저장하게 되면 Spring JPA에서 이 객체를 해당 데이터베이스의 테이블에 저장하게 된다.
  • @PostMapping 어노테이션으로 이 createEvent가 POST 요청을 처리하는 것으로 하였다. @RequestBody를 통해 Body에 있는 정보가 자동적으로 Event 객체로 역직렬화되어 매핑된다. 
  • linkTo 메서드를 통해 현 EventController의 URL에 대한 링크 정보를 손쉽게 추가할 수 있다. 여기서는 save된 객체에서 자동적으로 할당된 ID가 포함된 URL을 Header에 넣어서 반환한다.
  • ResponseEntity는 HTTP 응답에 대한 정보를 가지고 있는 객체다.
package com.example.springrestapi.events;

import org.springframework.data.jpa.repository.JpaRepository;

public interface EventRepository extends JpaRepository<Event, Integer> {
}

 

  • EventRepository를 JpaRepository 인터페이스를 위와 같이 상속하게 되면 자동적으로 IoC 컨테이너에 해당 빈이 등록되어 다른 객체에 주입될 수 있다. 

 

결과 화면

 

 

요청

MockHttpServletRequest:
HTTP Method = POST
Request URI = /api/events/
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/hal+json;charset=UTF-8"]
Body = {"id":null,"name":"Spring","description":"REST API Development","beginEnrollmentDateTime":"2010-11-23T14:23:00","closeEnrollmentDateTime":"2018-11-30T14:23:00","beginEventDateTime":"2018-12-05T14:30:00","endEventDateTime":"2018-12-06T14:30:00","location":"D Start up Factory","basePrice":100,"maxPrice":200,"limitOfEnrollment":100,"offline":false,"free":false,"eventStatus":null}
Session Attrs = {}

 

응답

MockHttpServletResponse:
Status = 201
Error message = null
Headers = [Location:"http://localhost/api/events/1", Content-Type:"application/hal+json;charset=UTF-8"]
Content type = application/hal+json;charset=UTF-8
Body = {"id":1,"name":"Spring","description":"REST API Development","beginEnrollmentDateTime":"2010-11-23T14:23:00","closeEnrollmentDateTime":"2018-11-30T14:23:00","beginEventDateTime":"2018-12-05T14:30:00","endEventDateTime":"2018-12-06T14:30:00","location":"D Start up Factory","basePrice":100,"maxPrice":200,"limitOfEnrollment":100,"offline":false,"free":false,"eventStatus":null}
Forwarded URL = null
Redirected URL = http://localhost/api/events/1
Cookies = []

 

참조: https://www.inflearn.com/course/spring_rest-api/#

소스 코드 : https://github.com/engkimbs/spring-rest-api



출처: https://engkimbs.tistory.com/858?category=789178 [새로비]