Backend
Spring Boot Test
2017년 8월 28일
원문에서 보기 ↗간단히 Spring Boot의 테스트 기능에 대해서 정리한 글입니다. 그저 Spring Boot 공식 문서를 정리한 수준이지만 앞으로 Spring Boot 에플리케이션을 개발하고 테스트하실 때 많은 도움이 되었으면 합니다~
Spring Boot에서 테스트를
Spring Boot는 애플리케이션을 테스트하기 위한 많은 기능을 제공합니다. Spring boot에서 테스트 모듈은 spring-boot-test와 spring-boot-test-autoconfigure가 존재합니다. 대부분의 경우는 spring-boot-starter-test만으로도 충분하며, spring-boot-starter-test는 spring boot의 테스트에 사용되는 Starter 패키지입니다. spring-boot-starter-test는 JUnit는 물론이고, AssertJ, Hamcrest를 포함한 여러 유용한 라이브러리를 포함하고 있습니다.
포함되어있는 주요 라이브러리들
기존 Spring framework에서 사용하던 spring-test 이외에도 여러 유용한 라이브러리를 포함하고 있습니다. 가지고 있는 라이브러리에는 Mocktio도 포함되어있는데 기본적으로 Mocktio 1.x 버전을 사용합니다. 하지만 원한다면 2.x 버전을 사용하는 것도 가능합니다.
- JUnit
- Spring Test & Spring Boot Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
@SpringBootTest
spring-boot-test는 @SpringBootTest라는 어노테이션을 제공합니다. 이 어노테이션을 사용하면 테스트에 사용할 ApplicationContext를 쉽게 생성하고 조작할 수 있습니다. 기존 spring-test에서 사용하던 @ContextConfiguration의 발전된 기능이라고 할 수 있습니다. @SpringBootTest는 매우 다양한 기능을 제공합니다. 전체 빈 중 특정 빈을 선택하여 생성한다든지, 특정 빈을 Mock으로 대체한다든지, 테스트에 사용할 프로퍼티 파일을 선택하거나 특정 속성만 추가한다든지, 특정 Configuration을 선택하여 설정할 수도 있습니다. 또한, 주요 기능으로 테스트 웹 환경을 자동으로 설정해주는 기능이 있습니다. 앞에서 언급한 다양한 기능들을 사용하기 위해서 첫 번째로 가장 중요한 것은 @SpringBootTest 기능은 반드시 @RunWith(SpringRunner.class)와 함께 사용해야 한다는 점입니다.
Bean
@SpringBootTest 어노테이션을 사용하면 테스트에 사용할 빈을 아주 손쉽게 생성할 수 있습니다. @SpringBootTest 어노테이션은 classes라는 속성을 제공합니다. 해당 속성을 통해서 빈을 생성할 클래스들을 지정할 수 있습니다. classes 속성에 @Configuration 어노테이션을 사용하는 클래스가 있다면 내부에서 @Bean 어노테이션을 통해서 생성되는 빈도 모두 등록이 됩니다. 만일 classes 속성을 통해서 클래스를 지정하지 않으면 애플리케이션 상에 정의된 모든 빈을 생성합니다.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {ArticleServiceImpl.class, CommonConfig.class})
public class SomeClassTest {
// Service로 등록하는 빈
@Autowired
private ArticleServiceImpl articleServiceImpl;
// CommonConfig에서 생성하는 빈
@Autowired
private RestTemplate restTemplate;
}
TestConfiguration
기존에 정의했던 Configuration을 커스터마이징하고 싶은 경우 TestConfiguration 기능을 사용할 수 있습니다. TestConfiguration은 ComponentScan 과정에서 생성될 것이며 해당 자신이 속한 테스트가 실행될때 정의된 빈을 생성하여 등록할 것입니다.
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestConfigArticleServiceImplTest {
@MockBean
private ArticleDao articleDao;
@Autowired
private RestTemplate restTemplate;
@Autowired
private ArticleServiceImpl articleServiceImpl;
@Test
public void test() {
String good = restTemplate.getForObject("test", String.class);
assertThat(good).isEqualTo("Good");
}
@TestConfiguration
public static class TestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate() {
@Override
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
System.out.println("Good");
if (responseType == String.class) {
return (T) "Good";
} else {
throw new IllegalArgumentException();
}
}
};
}
}
}
ComponentScan을 통해서 감지되기 때문에 만일 @SpringBootTest의 classes 속성을 이용하여 특정 클래스만을 지정했을 경우에는 TestConfiguation은 감지되지 않습니다. 그런 경우 classes 속성에 직접 TestConfiguration을 추가해주어야 합니다. 하지만 더 좋은 방법은 @Import 어노테이션을 사용하는 것입니다. @Import 어노테이션을 통해서 직접 사용할 TestConfiguration을 명시할 수 있으며 특정 테스트 클래스의 내부 클래스가 아닌 별도의 클래스로 분리하여 여러 테스트에서 공유할 수도 있습니다.
@TestConfiguration
public class TestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate() {
@Override
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
System.out.println("Good");
if (responseType == String.class) {
return (T) "Good";
} else {
throw new IllegalArgumentException();
}
}
};
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ArticleServiceImpl.class)
@Import(TestConfig.class)
public class TestConfigArticleServiceImplTest {
@MockBean
private ArticleDao articleDao;
@Autowired
private RestTemplate restTemplate;
@Autowired
private ArticleServiceImpl articleServiceImpl;
@Test
public void test() {
String good = restTemplate.getForObject("test", String.class);
assertThat(good).isEqualTo("Good");
}
}
MockBean
spring-boot-test 패키지는 Mockito를 포함하고 있기 때문에 기존에 사용하던 방식대로 Mock 객체를 생성해서 테스트하는 방법도 있지만, spring-boot-test에서는 새로운 방법도 제공하고 있습니다. @MockBean 어노테이션을 사용해서 이름 그대로 Mock 객체를 빈으로써 등록할 수 있습니다. 그렇기 때문에 만일 @MockBean으로 선언된 빈을 주입받는다면(@Autowired 같은 어노테이션 등을 통해서) Spring의 ApplicationContext는 Mock 객체를 주입해줍니다. 새롭게 @MockBean을 선언하면 Mock 객체를 빈으로써 등록하지만, 만일 @MockBean으로 선언한 객체와 같은 이름과 타입으로 이미 빈으로 등록되어있다면 해당 빈은 선언한 Mock 빈으로 대체됩니다.
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ArticleServiceImpl.class)
public class ArticleServiceImplTest {
@MockBean
private RestTemplate restTemplate;
@MockBean
private ArticleDao articleDao;
@Autowired
private ArticleServiceImpl articleServiceImpl;
@Test
public void testFindFromDB() {
List<Article> expected = Arrays.asList(
new Article(0, "author1", "title1", "content1", Timestamp.valueOf(LocalDateTime.now())),
new Article(1, "author2", "title2", "content2", Timestamp.valueOf(LocalDateTime.now())));
given(articleDao.findAll()).willReturn(expected);
List<Article> articles = articleServiceImpl.findFromDB();
assertThat(articles).isEqualTo(expected);
}
}
Properties
Spring Boot는 기본적으로 클래스 경로상의 application.properties(또는 application.yml)를 통해 애플리케이션 설정을 수행합니다. 하지만 테스트 중에는 설정이 기존과 달라질 필요가 있는 경우가 많은데 이때를 위한 기능을 SpringBootTest에서 제공하고 있습니다. SpringBootTest는 properties라는 속성이 존재합니다. 이 속성을 이용해서 별도의 테스트를 위한 application.properties(또는 application.yml)을 지정할 수 있습니다.
@RunWith(SpringBoot.class)
@SpringBootTest(properties = "classpath:application-test.yml")
public class SomeTest {
...
}
Web Environment test
앞에서 언급했듯이 @SpringBootTest 어노테이션을 사용하면 손쉽게 웹 테스트 환경을 구성할 수 있습니다. @SpringBootTest의 webEnvironment 파라미터를 이용해서 손쉽게 웹 테스트 환경을 선택할 수 있습니다. 제공하는 설정값은 아래와 같습니다.
- MOCK
- WebApplicationContext를 로드하며 내장된 서블릿 컨테이너가 아닌 Mock 서블릿을 제공합니다. @AutoConfigureMockMvc 어노테이션을 함께 사용하면 별다른 설정 없이 간편하게 MockMvc를 사용한 테스트를 진행할 수 있습니다.
- RANDOM_PORT
- EmbeddedWebApplicationContext를 로드하며 실제 서블릿 환경을 구성합니다. 생성된 서블릿 컨테이너는 임의의 포트는 listen합니다.
- DEFINED_PORT
- RAMDOM_PORT와 동일하게 실제 서블릿 환경을 구성하지만, 포트는 애플리케이션 프로퍼티에서 지정한 포트를 listen합니다(application.properties 또는 application.yml에서 지정한 포트).
- NONE
- 일반적인 ApplicationContext를 로드하며 아무런 서블릿 환경을 구성하지 않습니다.
TestRestTemplate
@SpringBootTest와 TestRestTemplate을 사용한다면 편리하게 웹 통합 테스트를 할 수 있다. TestRestTemplate은 이름에서 알 수 있듯이 RestTemplate의 테스트를 위한 버전입니다. @SpringBootTest에서 Web Environment 설정을 하였다면 TestRestTemplate은 그에 맞춰서 자동으로 설정되어 빈이 생성됩니다.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RestApiTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void test() {
ResponseEntity<Article> response = restTemplate.getForEntity("/api/articles/1", Article.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
...
}
}
기존에 컨트롤러를 테스트하는 데 많이 사용되던 MockMvc와 어떤 차이가 있는지 궁금할 것입니다. 가장 큰 차이점이라면 Servlet Container를 사용하느냐 안 하느냐의 차이입니다. MockMvc는 Servlet Container를 생성하지 않습니다. 반면, @SpringBootTest와 TestRestTemplate은 Servlet Container를 사용합니다. 그래서 마치 실제 서버가 동작하는 것처럼(물론 몇몇 빈은 Mock 객체로 대체될 수는 있습니다) 테스트를 수행할 수 있습니다. 또한, 테스트하는 관점도 서로 다릅니다. MockMvc는 서버 입장에서 구현한 API를 통해 비즈니스 로직이 문제없이 수행되는지 테스트를 할 수 있다면, TestRestTemplate은 클라이언트 입장에서 RestTemplate을 사용하듯이 테스트를 수행할 수 있습니다.
트랜젝션
이때 주의해야 할 점이 있는데 바로 @Transactional 어노테이션입니다. spring-boot-test는 그저 spring-test를 확장한 것이기 때문에 @Test 어노테이션과 함께 @Transactional 어노테이션을 함께 사용하면 테스트가 끝날 때 rollback 됩니다. 하지만 RANDOM_PORT나 DEFINED_PORT로 테스트를 설정하면 실제 테스트 서버는 별도의 스레드에서 수행되기 때문에 rollback이 이루어지지 않습니다.
ApplicationContext 캐시
참고로 @SpringBootTest 기능으로 인해서 생성된 ApplicationContext를 캐시됩니다. 만약에 @SpringBootTest의 설정이 동일하다면 동일한 ApplicationContext를 사용하게 됩니다.
@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
컨트롤러에서 Future나 DeferredResult의 객체를 반환하면 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 어노테이션을 사용하면 마찬가지로 in-memory embedded database가 설정되며, 테스트를 위한 JdbcTemplate이 생성됩니다.
@DataMongoTest
최근 점점 많은 인기를 얻고 있는 NoSQL DB인 MongoDB에 대해서도 편리한 테스트 기능을 제공합니다. @DataMongoTest 어노테이션이 이를 위한 기능을 제공하며 설정하는 내용은 @DatajpaTest와 유사합니다. 위에 다른 데이터 테스트 모듈과 유사하게 in-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"%29%29
.andRespond(withSuccess(articleJson, MediaType.APPLICATION_JSON));
Article article = service.findOneFromRemote(1);
assertThat(article.getId()).isEqualTo(1);
}
}
