단위 테스트를 만드는 것은 좋다. 버그를 쉽게 찾을 수 있을 뿐더러 코트를 리팩토링 할 때에도 좀 더 효과적으로 할 수 있다. 하지만 완벽하게 단위테스트 케이스를 만들기는 쉽지 않다. 시간이 부족할 수도 있고, 빠진 케이스도 있을 수 있고.. 솔직히 만들기 귀찮아서 안만들 경우도 있을 것이다. 또 다른 이유는 어떻게 테스트를 만들까 하는 고민도 있을 수 있다. 일반적은 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들은 많으니 참고하거나 자신에 맞는 기능이 있다면 찾아서 사용하면 되겠다.