Spring Boot test annotation

오늘 이시간에는 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에 올라가 있으니 한번씩 돌려보도록 하자.

그럼 오늘은 이만!

junit Rules

단위 테스트를 만드는 것은 좋다. 버그를 쉽게 찾을 수 있을 뿐더러 코트를 리팩토링 할 때에도 좀 더 효과적으로 할 수 있다. 하지만 완벽하게 단위테스트 케이스를 만들기는 쉽지 않다. 시간이 부족할 수도 있고, 빠진 케이스도 있을 수 있고.. 솔직히 만들기 귀찮아서 안만들 경우도 있을 것이다. 또 다른 이유는 어떻게 테스트를 만들까 하는 고민도 있을 수 있다. 일반적은 Spring mvc(필자가 자주 사용하므로) 경우에는 패턴이 딱 정해져있다. Controller Service, Repository(DAO) 등 기본적인 테스트는 패턴이 비슷하기에 별 고민 없이 만들 수 있다. 하지만 라이브러리 또는 공통으로 사용할 목적으로 코드를 만든다면 테스트 코드를 만드는 작업은 고민을 조금 하면서 만들어야 할 지도 모른다. 서두가 길었다. 결론은 단위 테스트를 생활화 하자 이다.

오늘은 제목 그대로 junit의 Rule에 대해서 알아볼까 한다.
junit에서 rule이란 테스트 케이스 내에서 동작을 유연하게 추가하거나 재정의 할 수 있는 목적으로 만들어 졌다.

junit 에서 기본적으로 제공해주는 rule은 현재 15개 정도? 제공해주는데 그 중에서 필자가 많이 쓸거 같은 rule 몇개 정도만 알아볼 예정이다.

ExpectedException

ExpectedException rule 은 @Test(expected = Exception.class) 이거와 비슷한? 거의 동일한 기능을 가지고 있다.
코드를 살펴보자.

@Rule
public ExpectedException exception = ExpectedException.none();

@Test
public void findByemailTest() {
  exception.expect(EmailNotFoundException.class);
  given(mockRepository.findByEmail(anyObject())).willReturn(null);
  mockService.findByEmail("123123");
}

위와 같이 해당하는 에러가 발생시에는 테스트가 통과 된다. 비정상적인 경우일때 위와 같이 ExpectedException을 사용해서 처리하면된다.

TemporaryFolder

TemporaryFolder rule은 임시 폴더 혹은 파일을 만들고 테스트가 끝나는 동시에 폴더 혹은 파일을 삭제해준다. 아주 마음에 드는 녀석이다. 이것 또한 코드로 살펴보자.

@Test
public void fileDownloadTxt() throws IOException {
  final byte[] body = downloadTxt("/mock");
  final File file = writeTxt(body);

  //
  System.out.println(file.getAbsoluteFile());
}

public String createUrl(String context) {
  return "http://localhost:" + port + context;
}

protected File writeTxt(byte[] body) throws IOException {
  File txtFile = folder.newFile();
  OutputStream stream = new FileOutputStream(txtFile);
  try {
    stream.write(body);
  } finally {
    stream.close();
  }
  return txtFile;
}
private byte[] downloadTxt(String context) {
  return restTemplate.getForObject(createUrl(context), byte[].class);
}

위의 코드는 파일을 다운로드 하는 코드이다. 파일을 다운로드 후에 임시 파일를 생성하고 그 위치에 해당 파일을 쓰는 테스트 이다. 그림이나 동영상이라면 파일을 어떻게 검증하지?.. 만약 문자라면 파일에 있는 텍스트를 검증하면 되겠다. 위의 코드는 그냥 파일의 위치 정도만 출력해 줬다.
파일 혹은 폴더를 생성해서 테스트를 작성한다면 위의 클래스를 사용하면 되겠다.

ExternalResource

ExternalResource rule은 추상 클래스이다. 외부 Resource를 테스트 할 때 적당하다. 예를들어 파일, 소켓, 서버, 데이터베이스 연결등 외부 자원 테스트할 때 사용하면 된다. 바로 전에 알아본 TemporaryFolderExternalResource 의 구현체이다. 코드로 살펴보자.

public class ExternalResourceTest {

  @Rule
  public ExternalResource resource = new ExternalResource() {
    @Override
    protected void before() throws Throwable {
      server.connect();
    };

    @Override
    protected void after() {
      server.disconnect();
    };
  };

  @Test
  public void serverTest() {
    run(server);
  }
}

위에서 보시다시피 before 메스드와 after 메서드를 구현해 주면 된다. 메스드명을 보면 잘 알겠지만 before는 테스트가 시작되기전 after는 테스트가 끝난 후에 메서드가 호출 된다.

ErrorCollector

ErrorCollector rule 은 에러가 발생되더라도 계속 실행되며 마지막에 모든 에러를 수집해서 출력해주는 기능이다.


@Rule public ErrorCollector collector = new ErrorCollector(); @Test public void findByemailTest() { collector.addError(new Throwable("first error")); given(mockRepository.findByEmail(anyObject())).willReturn(new Account(1L,"wonwoo", "wonwoo@test.com")); final Account account = mockService.findByEmail("123123"); collector.checkThat(account.getEmail(), is("wonwoo")); collector.checkThat(account.getId(), is(2L)); }

만약에 위와 같은 코드가 있다고 가정하자. 다른 코드는 일단 무시하고 addError 메서드와 checkThat만 알면된다. addError 메서드는 에러가 났을 경우 해당하는 에러와 메시지를 같이 출력해주는 기능이다. 굳이 넣지 않아도 된다. 출력해주는 기능만 있으니..
checkThat 메서드는 두개의 데이터가 다르더라도 계속 테스트를 진행한다. 에러 메시지를 살펴보자.

java.lang.Throwable: first error

    at me.wonwoo.ErrorCollectorTest.findByemailTest(ErrorCollectorTest.java:32)
    ///...길어서 생략


java.lang.AssertionError: 
Expected: is "wonwoo"
     but: was "wonwoo@test.com"
Expected :wonwoo
Actual   :wonwoo@test.com

//중간 생략

java.lang.AssertionError: 
Expected: is <2L>
     but: was <1L>
Expected :is <2L>

Actual   :<1L>

에러 메시지는 위와 같다. 우리가 처음에 설정 했던 addError 의 메시지가 출력 되고 다음에는 모든 에러가 수집되어 출력 된다.
위에서는 한개의 에러만 메시지가 보이는데 (first error) 더 추가 하고 싶다면 아래와 같이 추가적으로 addError 를 사용하면 된다.

@Test
public void findByemailTest() {
  collector.addError(new Throwable("first error"));
  collector.addError(new Throwable("second error"));
  //나머지 생략
}

Timeout

오늘 마지막으로 알아볼 Timeout rule이다. 이 것은 클래스 그대로 Timeout을 설정하는 기능이다. 테스트 케이스가 기준 시간보다 오래 걸린다면 테스트 케이스가 실패한다. 사용법은 아주 간단하다.

@Rule
public Timeout testRule = new Timeout(1000, TimeUnit.MILLISECONDS);

@Test
public void testInfiniteLoop() {
  String log = null;
  log += "ran1";
  while (true) {}
}

설명할 것도 없이 소스만 봐도 알 것 같다. Timeout 을 1초 주었고 테스트 케이스는 무한루프니 무조건 테스트 케이스가 실패한다. 오래 걸리는 작업을 테스트할 때 유용한 기능인듯 싶다.

이렇게 우리는 junit Rules에 대해서 알아봤다. 물론 이런 Rules은 Custom해서 자기만의 Rule을 만들 수 도 있다. 지금 당장 만들 것은 생각이 나지 않아서 문서에 있는 소스를 그대로 복붙하였으니 참고해서 만들면 되겠다.

public class TestLogger implements TestRule {
  private Logger logger;

  public Logger getLogger() {
    return this.logger;
  }

  @Override
  public Statement apply(final Statement base, final Description description) {
    return new Statement() {
      @Override
      public void evaluate() throws Throwable {
        logger = Logger.getLogger(description.getTestClass().getName() + '.' + description.getDisplayName());
        base.evaluate();
      }
    };
  }
}
<strong>
@Rule
public TestLogger logger = new TestLogger();

@Test
public void checkOutMyLogger() {
  final Logger log = logger.getLogger();
  log.warn("Your test is showing!");
}

위와 같이 TestRule 인터페이스를 구현해 주면 된다. 아주 쉽게 custom하게 만들 수 있어 좋은 거 같다. 하지만 만들일이 있나 모르겠네..

이 외에도 junit에 기본적인 rule들은 많으니 참고하거나 자신에 맞는 기능이 있다면 찾아서 사용하면 되겠다.

Mockito 이용해서 Test를 해보자

필자는 예전에는 SI 시절에는 테스트 케이스를 전혀 작성하지 못했다. 물론 핑계일 수도 있지만 테스트까지 만들 시간적 여유가 없었던건지 아니면 주변 환경 때문인지는 모르겠지만 아무튼 테스트를 전혀 작성하지 못했다. 하지만 지금 회사에서는 되도록이면 테스트 케이스를 만들려고 노력중이다. 물론 빌드 배포 할 때 조금 시간이 걸리긴 하지만 그만큼 필요한 시간이라고도 생각한다. 근데 시간에 점차 지나고 유지보수를 하면서 테스트 케이스 작성도 소홀해져가는건 사실이다. 그만큼 꾸준한 노력이 필요하다.

TDD 같은 경우에는 테스트부터 작성한다고 하는데 아직 그정도 까지는 못하겠다. 개발하는 사람 마음이겠지만 필자경우에는 먼저 클래스가 완성되면 그때 테스트를 작성한다. 기능하나 만들고 테스트 작성하고 그런 사람도 있는가 반면 필자 처럼 하는 사람도 있을 듯하다.

개발초기에는 보통 mock 객체를 이용해서 테스트 작성을 많이한다. 그래서 오늘은 개발 초기 단계에서 mock 객체를 이용해서 테스트 케이스를 작성하는 법을 알아보자.

메이븐에 다음과 같이추가 하자.

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>1.10.19</version>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-core</artifactId>
        <version>1.3</version>
    </dependency>
</dependencies>

메이븐에는 junit, mockito 를 추가 하면된다. hamcrest는 옵션이다. 다른 좋은 라이브러리가 있다면 다른걸 써도 상관은 없을 듯하다. 근데 hamcrest 요게 좀 검증이 된 라이브러리 같으니 필자는 요걸 사용하겠다.

필자는 보통 Spring를 사용하기 때문에 일반적인 Spring layer 를 테스트한다는 기준으로 작성하겠다. 물론 꼭 그럴필요는 없다. 다른 자바 프레임웤이나 안드로이드 기타 다른 어떤걸 사용하더라도 자바만(jvm에 올라가는 언어면 될듯 싶다) 사용한다면 비슷한 테스트를 작성할 수 있다.

일단 어떤 비지니스 로직이 아래와 같이 있다고 가정하자. 아래와 같이 간단한 비지니스 로직도 있겠지만 보통은 그렇지 않다. 예제이므로 간단하게 작성하였다.

public class MockService {

  private final MockRepository mockRepository;

  public MockService(MockRepository mockRepository) {
    this.mockRepository = mockRepository;
  }

  public List<Account> findAll() {
    return mockRepository.findAll();
  }

  public Account findByname(String name) {
    final Account account = mockRepository.findByname(name);
    //어쩌구 저쩌구
    return account;
  }
}
public interface MockRepository {

  List<Account> findAll();

  Account findByname(String name);
}

Spring의 일반적인 레이어인 Controller – Service – Repository (DAO) 순으로 되어있다. 일반적인 방법이므로 꼭 그래야만하는 것은 아니다. 또한 위와 같은 레이어에 종속적인 것도 아니다. Service와 Repository라는 네임을 썼지만 필자가 Spring을 주로 사용하기 때문에 그렇게 네이밍을 한거 뿐이지 위와 비슷한 형태이면 가능하다.

일단 MockRepository 부터 테스트를 작성해보자. MockRepository 는 클래스가 아니라 인터페이스다. 구현체도 없는데 어떻게 테스트를 하지? 상관없다. 우리는 가짜객체를 만들 것이니 구현체는 필요 없다. 내부적으로는 바이트 코드를 조작한다.

@RunWith(MockitoJUnitRunner.class)
public class MockRepositoryTest {

  @Mock
  private MockRepository mockRepository;

  @Test
  public void findBynameTest() {
    //given
    given(mockRepository.findByname("wonwoo")).willReturn(new Account(1L, "wonwoo", "wonwoo@test.com"));
    //when
    final Account account = mockRepository.findByname("wonwoo");
    //then
    verify(mockRepository, times(1)).findByname("wonwoo");
    assertThat(account.getId(), is(1L));
    assertThat(account.getName(), is("wonwoo"));
    assertThat(account.getEmail(), is("wonwoo@test.com"));
  }
}

한개씩 살펴보자. MockRepository 를 mock 객체를 만들기 위해서는 위와 같이 @Mock 어노테이션을 달아주면된다. 그리고 Class 상단에 @RunWith(MockitoJUnitRunner.class)에 달아 주자. 물론 꼭 그래야만 하는 것은 아니다. 그건 아래에서 살펴보자.
given() 메서드 안에 있는 mockRepository.findByname() 메서드는 실제 메서드를 호출 하는 것은 아니고 저 메서드를 호출할때에 willReturn() 메서드 안에 있는 객체를 리턴한다는 뜻이다. 쉽게 말해서 mockRepository.findByname("wonwoo") 메서드를 호출 하면 new Account(1L, "wonwoo", "wonwoo@test.com") 이 객체를 무조건 리턴한다는 뜻이다.
그리고 나서 실제 위에서 지정했던 mockRepository.findByname("wonwoo") 메서드를 호출하면 new Account(1L, "wonwoo", "wonwoo@test.com") 객체를 리턴한다. 하지만 이것 또한 가짜 객체를 리턴한다. 왜냐하면 우리는 실제객체를 사용한 적이 없기 때문이다.
다음 나오는 verify(mockRepository, times(1)).findByname("wonwoo") 이것은 mockRepository에 있는 findByname("wonwoo")를 한번 호출 했다는 뜻이다. 만약 times(2) 으로 바꾼 다면 에러가 발생한다. 왜냐하면 우리는 한번밖에 호출 하지 않았기 때문이다.
findAllTest() 메서드는 각자 만들어서 실행해보자.

다음으로는 Service 클래스를 테스트를 해보자.

public class MockServiceTest {

  @Mock
  private MockRepository mockRepository;

  private MockService mockService;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
    mockService = new MockService(mockRepository);
  }

  @Test
  public void findBynameTest() {
    //given
    given(mockRepository.findByname("wonwoo")).willReturn(new Account(1L, "wonwoo", "wonwoo@test.com"));
    //when
    final Account account = mockService.findByname("wonwoo");
    //then
    verify(mockRepository, atLeastOnce()).findByname(anyString());
    assertThat(account.getId(), is(1L));
    assertThat(account.getName(), is("wonwoo"));
    assertThat(account.getEmail(), is("wonwoo@test.com"));
  }
}

위의 테스트 클래스에서는 @RunWith(MockitoJUnitRunner.class) 어노테이션이 없다. 위의 어노테이션 대신 setup 메서드에있는 MockitoAnnotations.initMocks(this) 메서드가 동일한 효과를 가져온다.
여기에선 mockRepository는 아까와 같은 가짜객체지만 mockService는 진짜 객체이다. 실제 new를 이용해서 생성하고 있다. 하지만 생성자에 가짜 객체인 mockRepository를 주입받고 있다. 그럼 실제 Service 계층에서 mockRepository를 사용하면 그때 given() 메서드에서 정의해둔 가짜 객체가 실행된다. 위의 코드를 다시 정리하자면 mockService.findByname("wonwoo")를 실제로 호출하면 mockService.findByname() 메서드 안에 있는mockRepository.findByname("wonwoo") 메서드를 호출하면 가짜 객체를 만들어서 리턴한다는 뜻이다.
위의 코드를 좀더 간략하게 만들 수 있다.

@RunWith(MockitoJUnitRunner.class)
public class InjectMockServiceTest {

  @Mock
  private MockRepository mockRepository;

  @InjectMocks
  private MockService mockService;

  //..
}

@InjectMocks 어노테이션을 사용하면 아까와 같은 동일한 효과를 가질 수 있다.

만약 이해가 잘 안된다면 각자가 findBynameTest() 메서드를 한번 만들어 보자.

아까와는 다르게 verify() 메서드 안에는 atLeastOnce() 이런 메서드가 존재한다. 이 뜻은 해당 메서드가 최소 1번이상 실행 되었는지를 검증하는 메서드이다. 이외에도 atLeast(), atLeast(), atMost() 메서드 등이 존재한다.
또한 위에서는 가짜 객체를 만들 때 특정 파라미터(“wonwoo”)를 넘겨주었는데 꼭 그럴필요 없다. 만약 어떤 값만 넘어 오기만 하면 되면 이렇게 작성하면 된다.

given(mockRepository.findByname(anyObject()))...
//anyString()
//anyInt()
//기타 등등

위와 같이 anyObject() 뿐만아니라 String, int, long, double등 기본적인 자료형은 다 있는 듯 하다. 심지어 map, set, list 들도 존재한다.

이렇게 오늘은 mockito를 이용해서 테스트를 하는 방법을 살펴봤다. 테스트를 생활하 하자!