포토로그



유닛테스트에서 시각과 시간 Java

  • 현실에서는 시간을 통제하지 못한다. 프로그램에서는 통제할 수 있지만 하지 않는다. 자바에서 현재 시각은 특별한 관리가 필요하지 않는 자원이기 때문에 Date 객체를 직접 생성해서 사용하거나System.currentTimeMillis를 호출하여 얻는다. 이 경우 외부에서 시간의 흐름이나 정밀도를 통제할 수 없기때문에 테스트하기 어렵다. 이 글에서는 시간을 통제하여 시간을 다루는 객체를 테스트 가능하게 만드는 방법을 소개한다.

1. 짧게 말하면

시계 객체를사용한다. 시계 클래스에 정적 멤버를 두고 시스템 시계로 초기화 한다. 보통 때에는 시스템 시계를 그대로 사용하고, 유닛 테스트할 때는 시스템 시계 대신에 가짜 시계를 설치한다. 가짜 시계를 통해 시각을 마음대로 바꿀 수 있게 하고, 정밀도도 조절할 수있게 한다.

2. 설명하자면

다음과 PostDaoTest에서 createAndFind 테스트가 실패한다. ObjectSupport에 equals,hasCode 등이 올바로 구현되어 있다. Post, PostDao에는 오류가 없다고 가정하면 무엇이 원인일 수 있겠는가?

public class Post extends ObjectSupport {
private int id;
private String title;
private String content;
private Date createdAt;
public Post(String title, String content) {
this.title = title;
this.content = content;
this.createdAt = new Date();
}
}

public class PostDao ... {
...
}

public class PostDaoTest {
...
@Test public createAndFind() {
Post post = new Post("Hello", "Everybody");
int id = dao.create(post);
post.setId(id);
assertEquals(post, dao.find(id));
}
}

createdAt속성은 MySQL의 datetime형으로 선언된 필드에 저장된다. MySQL datetime형은 초단위 정밀도를 제공한다. 자바Date의 정밀도는 밀리초이다. 이제 알겠는가? 123456789 != 123456000.

원인을 알았으니 어떻게 테스트를 수정해야할까?

  1. 테스트하지 않는다.
  2. createAt 필드만 빼고 테스트한다.
  3. equals를 구현하여 createAt 필드는 초단위 정밀도로 비교한다.

우선 네 가지가 떠오른다. 하나 씩 생각해보자.

  1. 테스할 필요가 없을 정도의 코드에서야 문제 없겠지만, 그런 코드는 책의 예제로나 나오고, 게다가 우리는 대부분 책의 저자보다 똑똑하지 않다. 테스트가 필요하지 않는 코드는 없는 코드 밖에 없다 :-)

  2. title,content를 각기 assertEquals로 테스트하면 코드가 늘어난다. Post 객체를 위한 assertEquals를구현하면 중복코드는 줄어든다. 하지만 Post의 필드가 변경되면 고쳐야 할 곳이 한 곳 더 늘어난다.
  3. equals 정확하게 구현하기 어렵다. 구현해본 사람은 안다.

그럼 어떻게 해야할까? 문제를 좀 더 일반화시켜 보자. 현재 시각이 필요한 객체를 어떻게 테스트하는가? 널리 알려진 대로 new Date()System.currentTimeMillis()를 직접 호출하지 않고 시계 객체를 만들어 사용한다. 테스트할 때는 가짜 시계 객체를 제공하면 우리는 시간을 통제(우와!)할 수 있다. 위의 코드를 외부에서 제공되는 시계를 사용하도록 다시 구현한다.

public class Post extends ObjectSupport {
private int id;
private String title;
private String content;
private Date createdAt;
public Post(String title, String content) {
this.title = title;
this.content = content;
this.createdAt = Clock.INSTALLED.nowAsDate();
}
}

public class PostDaoTest {
@Test public createAndFind() {
FrozenClock clock = new FrozenClock();
Clock.install(clock);
Post post = new Post("Hello", "Everybody");
int id = dao.create(post);
post.setId(id);
assertEquals(post, dao.find(id));
}
}

이제 시간을 통제할 수 있으니 시간의 정밀도로 통제할 수 있다. 멈춘 시계를 설치할 수 있다면 정밀도가 낮은 시계도 만들어서 설치할 수 있다. 정밀도가 낮은 시계를 만들어서 설치하면 테스트를 통과한다.

public class PostDaoTest {
@Test public createAndFind() {
FrozenClock clock = new FrozenClock();
SecondClock dbClock = new SecondClock(clock);
Clock.install(dbClock);
Post post = new Post("Hello", "Everybody");
int id = dao.create(post);
post.setId(id);
assertEquals(post, dao.find(id));
}
}

문제가 해결되었다. 아래 시계 클래스들은 시간 정밀도가 다른 경우뿐만 아니라 시간을 다루는 객체의 테스트에 유용하게 사용할 수 있다.

public class Clock {
public static Clock INSTALLED = new SystemClock();
public static void install(Clock clock) {
INSTALLED = clock;
}

public Date nowAsDate() { return new Date(now); }
public abstract long now();

public static class SystemClock extends Clock {
public long now() { return System.currentTimeInMillis(); }
}
}

public class FrozenClock extends Clock {
private long now;
public FrozenClock() { this(System.currentTimeInMillis()); }
public FrozenClock(long now) { this.now = now; }
public long now() { return now; }
public void forward(long delta) { now += delta; }
public void rewind(long delta) { now -= delta; }
}

public class SecondClock extends Clock {
private Clock clock;
public SecondClock(Clock clock) { this.clock = clock; }
public long now() { return (clock.now() / 1000) * 1000; }
}


덧글

댓글 입력 영역