Spring Boot에서 테스트를 - 2

Spring Boot에서 테스트를 - 2

이전 문서에서는 Spring Boot에 새롭게 추가된 spring-boot-test-starter에 포함되어있는 모듈들과 기본적인 Spring Boot 테스트 모듈에 대해서 알아보았습니다. 이번에는 좀 더 세부적으로 들어가서 JSON 테스트, WebMvc 테스트, JPA 테스트, JDBC 테스트, Mongo 테스트, RestClient 테스트에 대해서 알아볼 것 입니다.

@JsonTest

@JsonTest 어노테이션을 사용하면 보다 편하게 JSON serialization과 deserialization을 테스트해볼 수 있습니다. @JsonTest 어노테이션은 ObjectMapper@JsonComponent 빈을 포함한 Jackson의 테스트를 위한 모듈들을 자동으로 설정합니다. 테스트를 위한 빈으로 JacksonTester, GsonTester, BasicJsonTester 등이 있습니다. 이를 주입받아서 사용하면 보다 편리하게 JSON을 테스트해볼 수 있습니다. 그리고 Assertj는 JSON을 위한 기능들을 제공합니다(JSONassert, JsonPath를 기반으로한). 아래는 JSON serialize와 deserialize를 테스트하는 예제입니다.

@RunWith(SpringRunner.class)
@JsonTest
public class ArticleJsonTest {
    @Autowired
    private JacksonTester<Article> json;

    @Test
    public void testSerialize() throws IOException {
        Article article = new Article(
                1,
                "kwseo",
                "good",
                "good article",
                Timestamp.valueOf((LocalDateTime.now())));

        // assertThat(json.write(article)).isEqualToJson("expected.json");  직접 파일과 비교
        assertThat(json.write(article)).hasJsonPathStringValue("@.author");
        assertThat(json.write(article))
                .extractingJsonPathStringValue("@.title")
                .isEqualTo("good");
    }

    @Test
    public void testDeserialize() throws IOException {
        Article article = new Article(
                1,
                "kwseo",
                "good",
                "good article",
                new Timestamp(1499655600000L));
        String jsonString = "{\"id\": 1, \"author\": \"kwseo\", \"title\": \"good\", \"content\": \"good article\", \"createdDate\": 1499655600000}";

        assertThat(json.parse(jsonString)).isEqualTo(article);
        assertThat(json.parseObject(jsonString).getAuthor()).isEqualTo("kwseo");
    }
}

@WebMvcTest

이전 문서에서 client-side에서 API를 테스트하는 TestRestTemplate을 살펴봤다면, 이번에는 server-side에서 API를 테스트하는 @WebMvcTest 어노테이션에 대해서 알아볼 것 입니다. 해당 어노테이션은 기존에 spring-test에서 컨트롤러를 테스트할 때 많이 사용하던 MockMvc에 관한 설정을 자동으로 수행해주는 어노테이션입니다. @WebMvcTest 어노테이션을 사용하면 테스트에 사용할 @Controller 클래스와 @ControllerAdvice, @JsonComponent, @Filter, WebMvcConfigurer, HandlerMethodArgumentResolver 등을 스캔합니다. 그리고 MockMvc를 자동으로 설정하여 빈으로 등록합니다.

@RunWith(SpringRunner.class)
@WebMvcTest(ArticleApiController.class)
public class ArticleApiControllerTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private ArticleService articleService;

    @Test
    public void testGetArticles() throws Exception {
        List<Article> articles = asList(
                new Article(1, "kwseo", "good", "good content", now()),
                new Article(2, "kwseo", "haha", "good haha", now()));

        given(articleService.findFromDB(eq("kwseo"))).willReturn(articles);

        mvc.perform(get("/api/articles?author=kwseo"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("@[*].author", containsInAnyOrder("kwseo", "kwseo")));
    }

    private Timestamp now() {
        return Timestamp.valueOf(LocalDateTime.now());
    }
}

Async Web Test

컨트롤러에서 FutureDeferredResult의 객체를 반환하면 HTTP 요청과 응답은 비동기로 동작합니다. 기존과 다른 방식으로 동작하기에 MockMvc로 테스트 방법도 약간의 변화가 필요합니다.

    ...
    @Test
    public void testGetArticle() throws Exception {
        Article expected = new Article(1, "kwseo", "good", "good content", now());

        given(articleService.findOneFromRemote(eq(1))).willReturn(expected);

        MvcResult result = mvc.perform(get("/api/articles/1")).andReturn();
        mvc.perform(asyncDispatch(result))      // asyncDispatch 필요
            .andExpect(status().isOk())
            .andExpect(jsonPath("@.id").value(1));
    }
    ...

위 코드처럼 MockMvc로 요청을 한 뒤 MvcResult로 받어서 asyncDispatch로 감싸줄 필요가 있습니다.

@DataJpaTest

Spring Data JPA를 테스트하고자 한다면 @DataJpaTest 기능을 사용해볼 수 있습니다. 이 어노테이션과 함께 테스트를 수행하면 기본적으로 in-memory embedded database를 생성하고 @Entity 클래스를 스캔합니다. 일반적인 다른 컴포넌트들은 스캔하지 않습니다. 참고로 @DataJpaTest@Transactional 어노테이션을 포함하고 있습니다. 그래서 테스트가 완료되면 자동으로 롤백하을 하기 위해서 직접 @Transactional 어노테이션을 달아줄 필요가 없습니다. 그런데 만약 @Transactional 기능이 필요하지 않다면 아래와 같이 줄 수 있습니다.

@RunWith(SpringRunner.class)
@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class SomejpaTest {
    ...
}

@DataJpaTest 기능을 사용하면 @Entity를 스캔하고 repository를 설정하는 것 이외에도 테스트를 위한 TestEntityManager라는 빈이 생성됩니다. 이 빈을 사용해서 테스트에 이용한 데이터를 정의할 수 있습니다. 아래는 @DataJpaTest를 사용하여 테스트를 수행하는 예제입니다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class ArticleDaoTest {
    @Autowired
    private TestEntityManager entityManager;
    @Autowired
    private ArticleDao articleDao;

    @Test
    public void test() {
        Article articleByKwseo = new Article(1, "kwseo", "good", "hello", Timestamp.valueOf(LocalDateTime.now()));
        Article articleByKim = new Article(2, "kim", "good", "hello", Timestamp.valueOf(LocalDateTime.now()));
        entityManager.persist(articleByKwseo);
        entityManager.persist(articleByKim);


        List<Article> articles = articleDao.findByAuthor("kwseo");
        assertThat(articles)
                .isNotEmpty()
                .hasSize(1)
                .contains(articleByKwseo)
                .doesNotContain(articleByKim);
    }
}

만약 테스트에 in-memory embedded database를 사용하지 않고 real database를 사용하고자 하는 경우, @AutoConfigureTestDatabase 어노테이션을 사용하면 손쉽게 설정할 수 있습니다.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class SomeJpaTest {
    ...
}

@JdbcTest

Spring Data JPA를 사용하지 않더라도 데이터베이스 테스트를 해볼 수 있습니다. @JdbcTest@DataJpaTest와 비슷한 설정을 수행하지만 순수 JDBC 테스트를 준비합니다. @JdbcTest 어노테이션을 사용하면 마찬가지로 im-memory embedded database가 설정되며, 테스트를 위한 JdbcTemplate이 생성됩니다.

@DataMongoTest

최근 점점 많은 인기를 얻고 잇는 NoSQL DB인 MongoDB에 대해서도 편리한 테스트 기능을 제공합니다. @DataMongoTest 어노테이션이 이를 위한 기능을 제공하며 설정하는 내용은 @DatajpaTest와 유사합니다. 위에 다른 데이터 테스트 모듈과 유사하게 im-memory embedded MongoDB를 사용하지만, @DataMongoTest@Entity가 아닌 @Document를 스캔하며 MongoTemplate을 생성합니다.

@RunWith(SpringRunner.class)
@DataMongoTest
public class SomeMongoTest {
    @Autowired 
    private MongoTemplate mongoTemplate;
    ...
}

만일 in-memory embedded MongoDB를 사용하는 것은 원하지 않고 외부에 직접 구축한 MongoDB을 사용하고자 한다면 아래와 같이 속성을 추가하면 됩니다.

@DataMongoTest(excludeAutoConfiguration = EmbeddedMongoAutoConfiguration.class)

@RestClientTest

@RestClientTest 기능은 자신이 서버 입장이 아니라 클라이언트 입장이 되는 코드를 테스트할때 유용합니다. 예를 들면, Apache HttpClient나 Spring의 RestTemplate을 사용하여 외부 서버에 웹 요청을 보내는 경우가 있습니다. @RestClientTest는 요청에 반응하는 가상의 Mock 서버를 만든다고 생각하면 됩니다. 내부 코드에서 웹 요청이 발생할 경우 @RestClientTest로 인해서 생성된 가상의 서버가 응답을 해줍니다. 물론 그 가상의 서버가 어떤식으로 응답을 할지 정의할 수 있습니다. 이를 사용하면 보다 RestTemplate 같은 객체를 Mock 객체로 바꿔서 테스트하는 것보다 리얼 환경에 가깝게 단위 테스트를 수행할 수 있습니다. 이 기능을 사용하면 자동으로 MockRestServiceServer라는 빈이 생성되며 이를 이용하면 손쉽게 요청과 응답에 대한 설정을 할 수 있습니다.

@RunWith(SpringRunner.class)
@RestClientTest(ArticleServiceImpl.class)
public class ArticleServiceImplWithRestClientTest {
    @MockBean
    private ArticleDao dao;
    @Autowired
    private ArticleServiceImpl service;
    @Autowired
    private MockRestServiceServer server;

    @Test
    public void testGetFindOneFromRemote() throws Exception {
        String articleJson = "{ \"id\": 1, \"author\": \"kwseo\", \"title\": \"gogogo\", \"content\": \"good\", \"date\": 1502322765 }";

        server.expect(requestTo("http://sample.com/some/articles/1"))
            .andRespond(withSuccess(articleJson, MediaType.APPLICATION_JSON));

        Article article = service.findOneFromRemote(1);
        assertThat(article.getId()).isEqualTo(1);
    }
}