오늘 이시간에는 Spring boot의 Test annotation 들을 살펴볼 예정이다. Spring boot 에서는 Test도 쉽게 할 수 있도록 많이 지원해주고 있다.
예전에 Spring Boot 1.4 Test에 보면 필자가 남긴 Spring boot 1.4에 추가된 내용이 있는데 조금 부실한 느낌이 있다. 현재 버전 (1.5.x) 버전으로 좀 더 추가된 내용을 포함해서 다시 살펴보도록 하자.

DataJpaTest

저번 포스팅에서도 언급을 했지만 Jpa를 Test 할수 있게 도와주는 어노테이션이다. 실제로 DataJpaTest 어노테이션을 사용할 경우에는 기본적으로 인메모리 데이터베이스가 존재해야 한다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class SpringDataJpaTest {
 // ..
}

위의 코드는 기본적으로 테스틀 할 수 있는 클래스이다. @DataJpaTest 어노테이션을 사용할 경우에는 Jpa에 필요한 클래스들만 로딩이 되어 좀 더 빠르게 테스트를 할 수 있는 장점이 있다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class SpringDataJpaTest {

  @Autowired
  private TestEntityManager testEntityManager;

  @Autowired
  private ReservationRepository reservationRepository;


  @Test
  public void findByEntityManagerReservationNameTest() {
    Reservation reservation = testEntityManager.persist(new Reservation("wonwoo"));
    Reservation rn = testEntityManager.find(Reservation.class, reservation.getId());
    assertThat(reservation.getReservationName()).isEqualTo(rn.getReservationName());
  }
}

@DataJpaTest를 사용할 경우에 TestEntityManager 클래스가 자동으로 빈으로 등록 된다. 그래서 우리는 위와 같이 TestEntityManager를 주입받아서 사용하면 된다. TestEntityManager는 Spring boot에서 만든 클래스이며 Jpa의 EntityManager와는 다르게 메서드가 많이 없지만 테스트하기에는 불편함없을 정도 있으니 문제는 없을 듯하다.

그러나 만약 실 데이터베이스에 테스트를 하고 싶다면 어떻게 할까? 아주 간단한게 어노테이션의 속성만 주면 메모리 데이터베이스가 아닌 데이터베이스에 테스트를 할 수 있다.

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

  @Autowired
  private TestEntityManager testEntityManager;

  @Autowired
  private ReservationRepository reservationRepository;

  //...
}

@AutoConfigureTestDatabase(replace = Replace.NONE) 라는 어노테이션의 속성을 주면 메모리 데이터 베이스가 아닌 실 데이터베이스에 테스트도 가능하다.

DataMongoTest

@DataJpaTest 와 동일하게 MongoDb도 쉽게 테스트할 수 있게 도와주는 어노테이션이다. 이 어노테이션은 Spring 1.5에 추가된 어노테이션이다. 참고로 2.0에는 좀 더 많은 nosql 을 테스트 할 수 있게 도와주는 어노테이션이 많이 추가 되었다. @DataJpaTest 어노테이션과 많이 비슷하므로 바로 코드로 보자.

@RunWith(SpringRunner.class)
@DataMongoTest
public class SpringDataMongoTest {

  @Autowired
  private AccountRepository accountRepository;

  @Autowired
  private MongoTemplate mongoTemplate;

  @Test
  public void findByFirstNameTest() {
    Account account = accountRepository.save(new Account("wonwoo", "lee"));
    Account at = accountRepository.findByFirstName("wonwoo");
    assertThat(account.getFirstName()).isEqualTo(at.getFirstName());
    assertThat(account.getLastName()).isEqualTo(at.getLastName());
  }
}

MongoRepository를 상속받은 인터페이스는 물론이고 MongoTemplate 도 자동으로 빈으로 등록된다. 그래서 아무 설정 없이도 위와 같이 코드를 작성할 수 있다.

JdbcTest

@JdbcTest 이 어노테이션도 @DataMongoTest 어노테이션과 마찬가지로 Spring boot 1.5에 나온 어노테이션이다. 이 어노테이션은 @DataJpaTest 어노테이션의 JPA와 관련된 설정을 제외한 설정으로 동작한다. Hibernate 설정과 JpaRepositories 의 설정을 제외하고 나머지는 동일하다. 사용법도 비슷하니 코드로 보자.

@RunWith(SpringRunner.class)
@JdbcTest
public class SpringJdbcTest {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Test
  public void findByReservationTest() {
    jdbcTemplate.update("insert into reservation (reservation_name) values(?)", "wonwoo");

    Reservation reservation = jdbcTemplate.queryForObject("select reservation_name from reservation where reservation_name = ?",
        (resultSet, i) -> new Reservation(resultSet.getString("reservation_name")), "wonwoo");
    assertThat(reservation.getReservationName()).isEqualTo("wonwoo");
  }

}

JdbcTemplate이 자동으로 빈으로 등록되어 우리는 JdbcTemplate를 주입만 받아서 사용하면 된다. 참 쉽게 잘 만들었다.

AutoConfigureRestDocs

테스트를 통해서 restDoc을 뽑을 수 있는 그런 어노테이션이다. 원하는 포맷에 맞게 markdown 혹은 asciidoctor 로 출력 할 수 도 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
@AutoConfigureRestDocs("target/generated-snippets")
public class UserDocumentationTests {

  @Autowired
  private MockMvc mvc;

  @MockBean
  private UserService userService;

  @Test
  public void getUserDocuments() throws Exception {
    given(userService.findByUser(any()))
        .willReturn(new User("wonwoo", "wonwoo@test.com"));
    this.mvc.perform(get("/users/{name}", "wonwoo").accept(MediaType.APPLICATION_JSON))
        .andDo(print())
        .andDo(document("{method-name}"))
        .andExpect(status().isOk());
  }
}

위와 같이 작성할 경우에 target/generated-snippets 경로 아래에 메서드명 기준으로 doc들이 출력되어 있을 것이다. 기본적으로 아무 설정 하지 않았을 경우에는 asciidoctor 파일들이 출력 된다.

httpie-request.adoc
curl-request.adoc
http-request.adoc
http-response.adoc

기본적으로 4개의 파일들이 출력 된다. 파일들의 내용은 한번씩 해봐서 보길..
만약 markdown으로 파일을 출력 하고 싶다면 아래와 같이 설정을 조금 해줘야 한다.

@TestConfiguration
static class ResultHandlerConfiguration implements RestDocsMockMvcConfigurationCustomizer {

  @Override
  public void customize(MockMvcRestDocumentationConfigurer configurer) {
    configurer.snippets().withTemplateFormat(TemplateFormats.markdown());
  }
}

위와 같이 설정할 경우에는 아까와 달리 adoc 확장자가 아니라 md 확장자로 변경되어 파일로 출력된다. 자세한 내용은 문서를 통해..

JsonTest

JsonTest 어노테이션은 json을 쉽게 serialize, deserialize를 테스트할 수 있도록 도와주는 어노테이션이다. 기본적으로는 BasicJsonTester가 빈으로 등록되며 클래스 패스에 jackson 혹은 gson이 있다면 자동으로 클래스들이 등록되어 사용할 수 있다. BasicJsonTesterserialize 밖에 테스트를 할 수 없는 것 같다. 약간 부족한 느낌이 들어 아마도 잘 사용하지 않을 듯하다.

@RunWith(SpringRunner.class)
@JsonTest
public class UserBasicJsonTests {

  @Autowired
  private BasicJsonTester json;

  @Test
  public void userDeserializeTest() throws Exception {
    assertThat(this.json.from("user.json"))
        .extractingJsonPathStringValue("@.name").isEqualTo("kevin");
  }
}

기본적으로는 이렇게 BasicJsonTester 클래스를 주입받아서 사용할 수 있다. 아무튼 별로다..

@RunWith(SpringRunner.class)
@JsonTest
public class UserJsonTests {

  @Autowired
  private JacksonTester<User> json;

  @Test
  public void userJsonTest() throws Exception {
    User user = new User("kevin", "kevin@test.com");
    assertThat(this.json.write(user)).isEqualToJson("user.json");
  }

  @Test
  public void userSerializeTest() throws Exception {
    User user = new User("kevin", "kevin@test.com");
    assertThat(this.json.write(user)).hasJsonPathStringValue("@.name");
    assertThat(this.json.write(user)).hasJsonPathStringValue("@.email");
    assertThat(this.json.write(user)).extractingJsonPathStringValue("@.name")
        .isEqualTo("kevin");
    assertThat(this.json.write(user)).extractingJsonPathStringValue("@.email")
        .isEqualTo("kevin@test.com");
  }

  @Test
  public void userDeserializeTest() throws Exception {
    String json = "{\"name\": \"kevin\", \"email\" : \"kevin@test.com\"}";
    assertThat(this.json.parse(json))
        .isEqualTo(new User("kevin", "kevin@test.com"));
    assertThat(this.json.parseObject(json).getName()).isEqualTo("kevin");
    assertThat(this.json.parseObject(json).getEmail()).isEqualTo("kevin@test.com");
  }
}

위의 코드는 JacksonTester 을 사용한 테스트 클래스이다. 아주 간편하게 테스트를 할 수 있다. jackson과 gson의 경우에는 serialize, deserialize 모두 테스트를 할 수 있기 때문에 유용한 듯 싶다. 만약 gson 으로 테스트를 하고 싶다면 아래와 같이 JacksonTester 를 GsonTester 변경해주기만 하면 된다.

@RunWith(SpringRunner.class)
@JsonTest
public class UserGsonTests {

  @Autowired
  private GsonTester<User> json;

  //...
}

WebMvcTest

WebMvcTest 어노테이션은 명 그대로 webmvc를 테스트할 수 있는 어노테이션이다. 이 어노테이션은 웹 관련 설정만 Spring이 로딩 한다. 예를들어 @ControllerAdvice, @JsonComponent, WebMvcConfigurer, Filter 등 웹과 관련있는 것들을 설정으로 로딩하므로 만약 로직상에서 다른 설정이 있다하면 제대로 동작하지 않을 수도 있으니 참고하면 되겠다.

사용법도 간단하니 살펴보자.

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTests {

  @Autowired
  private MockMvc mvc;

  @MockBean
  private UserService userService;

  @Test
  public void findBynameTest() throws Exception {
    given(userService.findByUser(any()))
        .willReturn(new User("wonwoo", "wonwoo@test.com"));
    this.mvc.perform(get("/users/{name}", "wonwoo").accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.name", is("wonwoo")))
        .andExpect(jsonPath("$.email", is("wonwoo@test.com")));
  }

  //...
}

@WebMvcTest 어노테이션에 해당하는 Controller 클래스를 넣어주면 된다. 그리고 나서 MockMvc 주입받아 테스트를 진행하면 된다. MockMvc 또한 @WebMvcTest 어노테이션에서 자동으로 설정해주니 따로 설정할 필요가 없다. 저번에도 언급한 내용이라 짧게 하겠다.

RestClientTest

RestClientTest 어노테이션은 외부의 어떤 Rest API를 사용할 때 mock 으로 대체할 수 있는 아주 유용한 테스트 어노테이션이다. 이 또한 사용법은 간단하다. 만약 외부에 어떤 리소스를 가져와야 한다면 아래와 같이 작성할 것이다.

@Component
public class UserService {

  private final RestTemplate restTemplate;

  public UserService (RestTemplateBuilder builder) {
    this.restTemplate = builder.build();
  }

  public User findByUser(String name) {
    return this.restTemplate.getForObject("/users/{name}",
        User.class, name);
  }
}

위의 RestTemplateBuilder 클래스는 Spring boot 1.4부터 추가된 클래스이다. 클래스패스에 라이브러리에 따라 자동으로 ClientHttpRequestFactory 생성해주고 메시지 컨버터도 알맞게 추가해준다.

@RunWith(SpringRunner.class)
@RestClientTest(UserService.class)
public class UserServiceTests {

  @Autowired
  private MockRestServiceServer server;

  @Autowired
  private UserService userService;

  @Test
  public void mockRestServiceTest() throws Exception{
    this.server.expect(
        requestTo("/users/wonwoo"))
        .andRespond(withSuccess(
            new ClassPathResource("user.json", getClass()),
            MediaType.APPLICATION_JSON));
    User user = this.userService.findByUser("wonwoo");
    assertThat(user.getName()).isEqualTo("wonwoo");
    assertThat(user.getEmail()).isEqualTo("wonwoo@test.com");
    this.server.verify();
  }
}

위와 같이 @RestClientTest 어노테이션에 해당하는 클래스를 작성후 테스트를 진행하면 된다. 실제로 API는 호출하지 않고 작성한 user.json 이라는 파일 형태의 json 형식으로 리턴해준다. 단위 테스트를 할 경우에 아주 유용한 어노테이션이다.

이렇게 오늘은 spring boot의 test annotation 에 대해서 살펴봤다. 한번씩 해보면 딱히 어려운 부분은 없는 것 같다. 하지만 익숙해져야 한다.
Spring boot에서는 이렇게 아주 유용한 Test annotation을 지원하니 한번씩 살펴보면 될 듯 싶다. 공식문서에도 잘 나와 있으니 참고하면 되겠다.

위의 코드는 github에 올라가 있으니 한번씩 돌려보도록 하자.

그럼 오늘은 이만!