ControllerTest - 책 저장하기

jihan kong·2022년 10월 12일
0

JUnit5

목록 보기
25/25
post-thumbnail

본 시리즈는 메타 코딩님의 Junit 강의를 학습한 내용을 바탕으로 정리하였습니다.

저번 포스팅을 끝으로 Controller 단의 구현도 모두 끝났다. 그렇다면 이대로 끝이 아니라 컨트롤러 단의 테스트 코드를 짜야한다. 테스트 코드의 중요성에 대해서 설명하자면 입만 아프니 바로 들어가보자.

위치는 Web 디렉토리의 BookApiControllerTest.java 이다.


saveBook_test( )

BookApiControllerTest.java

// 통합테스트 (C, S, R)
// 컨트롤러만 테스트하는 것이 아님
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookApiControllerTest {

    @Autowired
    private BookService bookService;

    @Autowired
    private TestRestTemplate rt;

    @Test
    public void saveBook_test() {
        // given	
		BookSaveReqDto bookSaveReqDto = new BookSaveReqDto();
        bookSaveReqDto.setTitle("스프링1강");
        bookSaveReqDto.setAuthor("겟인데어");
        
        // when

        // then
        
    }
}

우리는 현재 Controller, Service, Repository를 모두 테스트하는 통합테스트를 구현 중에 있다. 물론 mocking을 통해 Controller Test만 구현할 수도 있다.

먼저 우리에게 주어진 것은 BookSaveReqDto 이다. SaveReqDto 의 내용 즉, TitleAuthor 이 있어야 테스트를 진행할 수 있을 것이다. 먼저 세팅해놓자.

그런데 과연 SaveReqDto 의 내용을 컨트롤러가 이해할 수 있을까? 🤔

컨트롤러는 FrontController 의 도움을 받는다. 이 친구에게 한번 물어보자.

FrontController

1. 정의와 역할

각 컨트롤러는 공통 처리를 수행해주는 일종의 수문장 격인 FrontController 라는 것이 존재하는데, 이는 스프링에서 Dispatcher Servlet 이라는 이름으로 기능한다.

FrontController 즉, 스프링의 Dispatcher Servlet 의 역할은 각 컨트롤러가 가지고 있는 주소 (URL) ( /a, /b, /c, ... ,/f 등 ) 에 맞게 요청이 들어오면 그것을 알맞은 컨트롤러에 매칭시켜 전송하는 역할을 수행한다.

2. 클라이언트로부터의 요청시

클라이언트로부터 요청데이터가 들어오면 FrontController 는 Service와 직접적으로 통신하는 Controller단의 BookSaveReqDto 를 찾아 데이터를 저장하게 된다.

그러나, 요청 데이터의 형태는 json 이고 우리는 자바 기반 위에서 코딩을 하고 있다. 타입이 서로 불일치하는 현상이 발생하고 있다. 따라서 데이터를 자바면 자바, json이면 json 형태로 바꿔주는 작업이 필요하다.


ObjectMapper의 활용

BookApiControllerTest.java

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookApiControllerTest {

  // (..주입된 내용 생략)
  
  private static ObjectMapper om;

  @BeforeAll
  public static void init() {
     om = new ObjectMapper();
  }

json 을 자바가 이해할 수 있는 형태로 바꿔주기 위한 전처리 단계로 ObjectMapper 를 활용하자. json 데이터를 object로 감싸기 위함이다.


1. saveBook_test( ) (given 완료)

BookApiControllerTest.java

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookApiControllerTest {

    @Autowired
    private BookService bookService;

    @Autowired
    private TestRestTemplate rt;

    private static ObjectMapper om;		// 1.

    @BeforeAll							// 2.
    public static void init() {
        om = new ObjectMapper();
    }

    @Test
    public void saveBook_test() throws Exception {
        // given
        BookSaveReqDto bookSaveReqDto = new BookSaveReqDto();
        bookSaveReqDto.setTitle("스프링1강");
        bookSaveReqDto.setAuthor("겟인데어");

        String body = om.writeValueAsString(bookSaveReqDto);		// 3.
        System.out.println("==================");
        System.out.println(body);
        System.out.println("==================");
        // when

        // then
        
    }
}

<코드 설명>

  1. ObjectMapper 클래스를 static 으로 선언

    ObjectMapper 클래스는 왜 static으로 선언할까❓
    ObjectMapper 가 속해있는 Jackson 라이브러리는 non-static 으로 Inner class를 생성할 경우, 오류를 뿜어내는데 이는 기본 생성자가 없어 객체를 초기화 시키지 못하기 때문이라고 한다. 따라서 static 으로 선언해야한다.
    ( 출처 : https://klyhyeon.tistory.com/299 )

  2. 테스트를 진행하기 전에 ObjectMapperstatic 으로 새롭게 생성한다.

  3. ObjectMapper 메소드의 기능 중 writeValueAsString 을 통해 object를 JSON으로 바꿔준다. 이는 String 형태로 String body 변수에 담기게 된다.


이제 테스트를 진행해보면 다음과 같이 body의 내용이 잘 출력되는 것을 볼 수 있다.

모든 준비는 끝났다. object로 감쌌기 때문에 이를 가공해서 활용할 준비가 된 셈이다.


2. saveBook_test( ) (when 완료)

BookApiControllerTest.java

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class BookApiControllerTest {

    @Autowired
    private BookService bookService;

    @Autowired
    private TestRestTemplate rt; 		// 1. 

    private static ObjectMapper om;
    private static HttpHeaders headers; // 2.

    @BeforeAll
    public static void init() {
        om = new ObjectMapper();
        headers = new HttpHeaders();	
        headers.setContentType(MediaType.APPLICATION_JSON);
    }

    @Test
    public void saveBook_test() throws Exception {
        // given
        BookSaveReqDto bookSaveReqDto = new BookSaveReqDto();
        bookSaveReqDto.setTitle("스프링1강");
        bookSaveReqDto.setAuthor("겟인데어");

        String body = om.writeValueAsString(bookSaveReqDto);
        System.out.println("==================");
        System.out.println(body);
        System.out.println("==================");
        
        // when
        HttpEntity<String> request = new HttpEntity<>(body, headers);

		// 3.
        ResponseEntity<?> response = rt.exchange("/api/v1/book", HttpMethod.POST, request, String.class);
        
        // then
	}
}

<코드 설명>

  1. TestRestTemplate 은 REST 방식으로 개발한 API의 Test를 최적화 하기 위해 만들어진 클래스이다.
  2. API를 호출하기 위해 HttpHeaders 를 선언한다.
  3. 결과를 ResponseEntity로 반환받는다. Http header를 변경할 수 있다.

3. saveBook_test( ) (then 완료)

마지막으로 검증만 하면 테스트 코드 작성이 끝난다.

BookApiControllerTest.java

  // given (...생략)
  // when (...생략)
  // then
  DocumentContext dc = JsonPath.parse(response.getBody());
  String title = dc.read("$.title");
  String author = dc.read("$.author");

  assertThat(title).isEqualTo("스프링1강");
  assertThat(author).isEqualTo("겟인데어");
 }

모든 것이 끝났다. 이제 테스트를 돌려보자.

OMG😱
Exception이 터졌다. 오류 로그를 보니 jsonPath NotFoundException 이다. jsonPath를 인식하지 못하는 모양이다. 생소한 jsonPath 문법에 대해서 공부할 필요성이 있다.

그러나, 우리에겐 구 선생님이 있다. 구글에 "jsonPath 문법" 이라고 검색하면 첫번째로 나오는 페이지를 들여다보자.
( https://seongjin.me/how-to-use-jsonpath-in-kubernetes/ )

객체를 다룰 때에 최상단 노드는 $ 로, 하위 노드는 . 로 작성해야한다. 그러고보니 우리는 "$.title" 이라고 했는데, 중간에 body를 거쳐야하므로 . 을 통해 이를 표현해보자.

 DocumentContext dc = JsonPath.parse(response.getBody());
        String title = dc.read("$.body.title");
        String author = dc.read("$.body.author");

또 다른 오류 ⚠️

다음과 같이 이번엔 빌드 자체가 안되는 오류에 직면했는데 이 또한 타입에 관한 이슈인 것 같다.

  // when
  HttpEntity<String> request = new HttpEntity<>(body, headers);
  ResponseEntity<?> response = rt.exchange("/api/v1/book", HttpMethod.POST, request, String.class);
  
  System.out.println(response.getBody());

ResponseEntity<?> : Generic type을 이와 같이 세팅할 경우, 따로 지정해주지 않았기 때문에 일반적인 Object 타입으로 리턴된다.

ResponseEntity<?> 는 Object 타입.
그러나, body의 title, author는 모두 String 타입!
즉, 타입이 일치하지 않는다.

따라서 ResponseEntity<String> 으로 수정한다.

이제 전체 테스트를 돌려보면...

깔끔하게 초록불이 뜨는 모습이다. 🤗

profile
학습하며 도전하는 것을 즐기는 개발자

0개의 댓글