본문 바로가기

Spring Boot

스프링부트에서 REST API 테스트코드 작성하기

오늘은 테스트 코드에 대해서 끄적여볼까 한다

이 역시 나를 위한 포스팅이다. 까먹기 전에 기록해놔야지

 

package com.example.study.api;

import com.example.study.dto.BoardDto;
import com.example.study.entity.Board;
import com.example.study.repository.BoardRepository;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BoardApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private BoardRepository boardRepository;

    @After
    public void clean() {
        boardRepository.deleteAll();
    }

    @Test
    public void 게시글_저장() throws Exception{
        //given
        String title = "테스트 제목";
        String content = "테스트 본문";
        String author = "테스트 계정";

        BoardDto boardDto = BoardDto.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();

        String url = "http://localhost:"+port+"/api/board";

        //when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, boardDto, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Board> saved = boardRepository.findAll();
        assertThat(saved.get(0).getTitle()).isEqualTo(title);
        assertThat(saved.get(0).getContent()).isEqualTo(content);
        assertThat(saved.get(0).getAuthor()).isEqualTo(author);
    }

    @Test
    public void 게시글_수정() throws Exception{
        //given
        Board saved = boardRepository.save(Board.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long updatedId = saved.getId();

        String updatedTitle = "title1";
        String updatedContent = "content1";

        BoardDto boardDto = BoardDto.builder()
                .title(updatedTitle)
                .content(updatedContent)
                .build();

        String url = "http://localhost:"+port+"/api/board/"+updatedId;

        HttpEntity<BoardDto> requestEntity = new HttpEntity<>(boardDto);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Board> updated = boardRepository.findAll();
        assertThat(updated.get(0).getTitle()).isEqualTo(updatedTitle);
        assertThat(updated.get(0).getContent()).isEqualTo(updatedContent);
    }

    @Test
    public void 게시글_삭제() throws Exception{
        //given
        Board saved = boardRepository.save(Board.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long savedId = saved.getId();

        String url = "http://localhost:"+port+"/api/board/"+savedId;

        HttpEntity<Board> savedEntity = new HttpEntity<>(saved);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.DELETE, savedEntity, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Board> deleted = boardRepository.findAll();
        assertThat(deleted).isEmpty();
    }
}

 

전체 코드를 보면 다음과 같다

 

생성, 수정, 삭제 API에 대한 테스트 코드이다

나도 공부 중에 구글링, 책 보면서 참고한 코드들이라 이게 정답인지는 확실치는 않다

 

그럼 하나하나 살펴보자

 

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class BoardApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private BoardRepository boardRepository;

    @After
    public void clean() {
        boardRepository.deleteAll();
    }

 

● @RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

spring-boot-test 의존성을 추가하면 @SpringBootTest라는 어노테이션을 사용할 수 있다

이 어노테이션을 사용하면 테스트에 사용할 ApplicationContext를 쉽게 생성하고 조작할 수 있다

기존 spring-test에서 사용하던 @ContextConfiguration의 발전된 기능이라고 할 수 있다

@SpringBootTest는 매우 다양한 기능들을 제공한다

전체 빈 중 특정 빈을 선택하여 생성한다든지, 특정 빈을 Mock으로  대체한다든지, 테스트에 사용할 프로퍼티 파일을 선택하거나 특정 속성만 추가한다든지, 특정 Configuration을 선택하여 설정할 수도 있다

또한, 주요 기능으로 테스트 웹 환경을 자동으로 설정해주는 기능이 있다

앞에서 언급한 다양한 기능들을 사용하기 위해서 첫 번째로 가장 중요한 것은 @SpringBootTest 기능은 반드시

@RunWith(SpringRunner.class)와 함께 사용해야 한다는 점이다

 

WebEnvironment.RANDOM_PORT

@SpringBootTest의 webEnvironment 속성은 테스트 웹 환경을 설정하는 속성이며,

기본값은 SpringBootTest.WebEnvironment.MOCK이다

WebEnvironment.MOCK은 실제 서블릿 컨테이너를 띄우지 않고 서블릿 컨테이너를 mocking 한 것이 실행된다

이 속성 값을 사용할 때는 보통 MockMvc를 주입받아 테스트한다

스프링 부트의 내장 서버를 랜덤 포트로 띄우려면 webEnvironment를 SpringBootTest.WebEnvironment.RANDOM_PORT로 설정하면 된다

이 설정은 실제로 테스트를 위한 서블릿 컨테이너를 띄운다

WebEnvironment.MOCK을 사용할 때와는 달리 TestRestTemplate를 주입받아 테스트한다

 

TestRestTemplate

@SpringBootTest와 TestRestTemplate을 사용한다면 편리하게 웹 통합 테스트를 할 수 있다

TestRestTemplate은 이름에서 알 수 있듯이 RestTemplate의 테스트를 위한 버전이다

@SpringBootTest에서 Web Environment설정을 하였다면 TestRestTemplate은 그에 맞춰서 자동으로 설정되어 빈이 생성된다

 

 

1. 생성

 

@Test
    public void 게시글_저장() throws Exception{
        //given
        String title = "테스트 제목";
        String content = "테스트 본문";
        String author = "테스트 계정";

        BoardDto boardDto = BoardDto.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();

        String url = "http://localhost:"+port+"/api/board";

        //when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, boardDto, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Board> saved = boardRepository.findAll();
        assertThat(saved.get(0).getTitle()).isEqualTo(title);
        assertThat(saved.get(0).getContent()).isEqualTo(content);
        assertThat(saved.get(0).getAuthor()).isEqualTo(author);
    }

 

생성 API를 테스트하는 테스트 코드이다

코드를 보면 given, when, then 패턴으로 코드를 작성한 것이 보인다

간단하게 말하면 순서대로 각각 준비, 실행, 검증이다

given은 테스트를 위해 준비를 하는 과정이다.

테스트에 사용되는 변수, 입력 값 등을 정의한다

when은 실제로 액션을 하는 테스트를 실행하는 과정이다

보통 하나의 메서드만 수행하는 것이 바람직하기 때문에, when 가장 짧고 심플한 구문이다

마지막으로 then은 검증을 하는 과정이다

예상한 값, 실제 실행을 통해서 나온 값을 검증한다

 

 

2. 수정

 

@Test
    public void 게시글_수정() throws Exception{
        //given
        Board saved = boardRepository.save(Board.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long updatedId = saved.getId();

        String updatedTitle = "title1";
        String updatedContent = "content1";

        BoardDto boardDto = BoardDto.builder()
                .title(updatedTitle)
                .content(updatedContent)
                .build();

        String url = "http://localhost:"+port+"/api/board/"+updatedId;

        HttpEntity<BoardDto> requestEntity = new HttpEntity<>(boardDto);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Board> updated = boardRepository.findAll();
        assertThat(updated.get(0).getTitle()).isEqualTo(updatedTitle);
        assertThat(updated.get(0).getContent()).isEqualTo(updatedContent);
    }

 

다음은 수정 API 테스트이다

과정을 보면 다음과 같다

Repository에 데이터를 저장한다

그리고 Dto를 통해서 값을 변경한다

그 후에 HttpEntity에 데이터를 담고, 해당 메서드를 실행시킨다 (PUT)

마지막으로 responseEntity를 검증하면 끝!

근데 공부를 하던 중에 HttpEntity와 ResponseEntity가 정확히 뭔지 궁금해졌다

지금까지 사용했을 때는 저렇게 써야 하는 게 하나의 정답이라고 생각하며 써왔다

사실 그냥 생각 없이 썼다. 왜 써야하는지, 언제 뭘 써야하는지 모르고..

그래서 이참에 구글링으로 찾아봤다

 

HttpEntity

Spring에서는 HttpEntity라는 클래스를 제공한다

이 클래스는 Http 프로토콜을 이용하는 통신의 header와 body 관련 정보를 저장할 수 있게 한다

그리고 이를 상속받은 클래스로 RequestEntity와 ResponseEntity가 존재한다

즉, 통신 메시지 관련 header와 body의 값들을 하나의 객체로 저장하는 것이 HttpEntity클래스 객체이고

Request 부분일 경우 HttpEntity를 상속받은 RequestEntity가,

Response 부분일 경우 HttpEntity를 상속받은 ResponseEntity가 하게 된다

 

 

3. 삭제

 

@Test
    public void 게시글_삭제() throws Exception{
        //given
        Board saved = boardRepository.save(Board.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        Long savedId = saved.getId();

        String url = "http://localhost:"+port+"/api/board/"+savedId;

        HttpEntity<Board> savedEntity = new HttpEntity<>(saved);

        //when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.DELETE, savedEntity, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Board> deleted = boardRepository.findAll();
        assertThat(deleted).isEmpty();
    }

 

마지막으로 삭제 테스트이다

전체적으로 보면 위에 코드들과 큰 차이는 없다

 

 


 

오늘은 테스트 코드에 대해서 공부를 해봤다

아직 모르고 사용하는 기능들이 많은 거 같다

조급해하지 말고 하나하나 천천히 공부해가자!