필자는 예전에는 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를 이용해서 테스트를 하는 방법을 살펴봤다. 테스트를 생활하 하자!