다음과 같은 요구사항이 있다.
- 현재 시간이 0~12시이면 오전을 반환한다.
- 현재 시간이 12~24시이면 오후를 반환한다.
위 요구사항에 대한 테스트 코드를 구현하면 다음과 같다.
package net.slipp;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
public class DateMessageProviderTest {
@Test
public void 오전() throws Exception {
DateMessageProvider provider = new DateMessageProvider();
assertThat(provider.getDateMessage(), is("오전"));
}
@Test
public void 오후() throws Exception {
DateMessageProvider provider = new DateMessageProvider();
assertThat(provider.getDateMessage(), is("오후"));
}
}
위 테스트 코드에 대한 구현 코드는 다음과 같다.
package net.slipp;
import java.util.Calendar;
public class DateMessageProvider {
public String getDateMessage() {
Calendar now = Calendar.getInstance();
int hour = now.get(Calendar.HOUR_OF_DAY);
if (hour < 12) {
return "오전";
}
return "오후";
}
}
위와 같이 구현하고 테스트를 실행하니 다음과 같은 결과를 얻을 수 있었다.
지금 이 글을 쓰고 있는 시점이 오전이기 때문에 오전 메시지를 반환하는 요구사항은 만족했지만 오후 메시지에 대한 테스트는 실패했다. 만약 12시가 지나 오후가 되면 오후 메시지에 대한 테스트는 성공하는데 오전 메시지에 대한 테스트는 실패할 것이다.
위와 같은 요구사항을 만족하기 위해 단위 테스트를 구현해야 한다. 오전과 오후 두 가지 경우에 대한 단위테스트 어떻게 구현하면 모두 성공할 수 있을까?
두 가지 요구사항을 만족시킬 수 있는 모든 경우의 수를 찾아보자. 이 모든 경우의 수가 자바에서 단위 테스트를 할 때 활용할 수 있는 대부분이 될 것이라 생각한다.
@hyukhur 이 글의 목적은 자바로 위 코드를 단위 테스트할 수 있는 모든 방법을 찾아보는 거야. 딱 하나의 정답을 찾자는 것이 아니니 네가 생각하는 모든 방법을 찾아서 제시해도 된다. 프로그래밍이라는 것이 정답은 없고, 가장 최선의 답을 찾아가는 과정이니 다양한 방법을 생각해 보면 좋겠다.
13개의 의견 from SLiPP
Calender now=Calender.getInstance() 부분을 extra method 한 후 이 부분을 Test 에서 override 함. 또는 extract method 한 부분에 대해서 mockito 등 을 사용해 원하는 날짜를 리턴하게 함....
public String getDateMessage() public String getDateMessage(Calendar baseTime) 로 분리
첫번째 메소드는 현재 시간을 두전째 메소드로 전달만 함 두번째 메소드는 전달 받은 시간을 기준으로 오전과 오후를 구별함
첫번째 메소드에 대한 단위테스트는 "현재 시간"이 잘 넘어갔는지 테스트 두번째 메소드에 대한 단위테스트는 전달 받은 시간에 따라 "오전 오후" 결과가 잘 나오는지 테스트
단위테스트에서 단위를 위와 같이 자르면 테스트가 용이할 것 같습니다. 저는 일단 MockObject를 쓰지 않고 하는 것을 선호 합니다.
말씀하신 의도와 맞는지 모르겠네요.
@hyukhur 이 글의 목적은 자바로 위 코드를 단위 테스트할 수 있는 모든 방법을 찾아보는 거야. 딱 하나의 정답을 찾자는 것이 아니니 네가 생각하는 모든 방법을 찾아서 제시해도 된다. 프로그래밍이라는 것이 정답은 없고, 가장 최선의 답을 찾아가는 과정이니 다양한 방법을 생각해 보면 좋겠다.
@hyukhur 께서 말씀하신데로 public String getDateMessage(Calendar baseTime) 로 변경 하고 Calender 생성의 책임을 클라이언트 코드로 이동 시켜버림...
테스트하려는 환경 자체가 정말 오전/오후인 상황에서 점검하는 것이 어떨까요? Runtime.getRuntime().exec("cmd /C time " + strTimeToSet); // hh:mm:ss 내용을 junit에서 넣어주고 테스트(오전값 넣고 오전() 테스트, 오후 값 넣고 오후() 테스트...) 하면 어떨까요?
장점으로는 시스템 자체가 오전/오후의 환경을 만든 뒤에 테스트하게 되니, 테스트 대상 소스가 변경되지 않고 정확하게 테스트가 가능하게 되겠지만, 단점으로는 테스트 수행 환경을 테스트 소스가 변경하게 되는데, 이 부분은 단위테스트 소스의 권한을 넘는 작업인 듯 하네요. 테스트 끝나고 서버의 날짜가 이상하게 변경되는 단점도 있겠군요.
끝..... (리펙토링 요소는 남겨둠 ^^)
일단 저는 Calendar를 안써서... ㅎㅎ;;;;
@자바지기님 코드 변경 없이 테스트 방법을 생각해봐야하는 것인가요? 테스트 가능 여부보다 API 설계가 깔끔하게 되는 것이 더 중요하다고 생각하는데요. Calendar 를 mock object로 교체 하는 방법만 떠오르네요.
@hyukhur 위 댓글에도 이야기했지만 Production Code를 마음데로 변경해도 된다. 제약이 없다. 요구사항을 만족하도록 구현하면 된다. 위 코드 하나로 단위 테스트와 관련한 많은 이야기를 해볼 수 있어서..
그리고 Calendar에 시간을 set 할 수 있는 기능도 있는데 굳이 mock object를 활용해야 되나? mock object 없이도 테스트 가능하다고 생각한다. 깊이 있게 생각하면 정말 단순하게 문제를 풀 수 있는 방법도 있다. 이 코드에서 너무 Calendar API에 집착하지 않아도 된다.
'시간'에 따라 오전/오후를 반환하는지를 테스트해보면 될 것 같아요.
현재코드는 메소드 호출시점에 Calendar 객체의 현재시간을 가져와 오전/오후를 반환하도록 되어있어서 두가지 테스트가 함께 성공하지 못하네요.
요구사항은 날짜가 아닌 '시간'에 따른 오전/오후 반환이니깐요. getDateMessage() 메소드를 overloading 하던지, 단순 extract method 하던지해서 더 작은 모듈의 단위테스트를 수행하면 될 것 같아요.
package net.slipp;
import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*;
import org.junit.Test;
public class DateMessageProviderTest { @Test public void 오전() throws Exception { DateMessageProvider provider = new DateMessageProvider();
}
@Test public void 오후() throws Exception { DateMessageProvider provider = new DateMessageProvider();
} } ```
또 다른 방법은 뭐가 있을려나요?
아 더불어 경계점에 대한 테스트를 수행해야하는데 요구사항이 불명확하네요.ㅎㅎ
현재 시간이 0~12시이면 오전을 반환한다. 현재 시간이 12~24시이면 오후를 반환한다. 그럼 12시는 오전일까요~ 오후일까요~ 물론 코드상은 오후입니다만.^^;
@lark 맞습니다. 12시 경계 값에 대한 요구사항이 불명확합니다. 이 문제의 본질은 아니라 대충 처리했으니 너그러이 이해해 주세요. 위 문제와 관련해 여러 가지 해결 방법이 있겠지만 위 코드로만 봤을 때 공유해 주신 내용이 가장 좋은 단위 테스트 방법이라 생각합니다. 물론 앞의 댓글에서도 언급했지만 이 문제는 정답을 찾자는 것이 아니라 여러 가지 해결 방법을 고민해 보자는 것이었습니다.
그럼 위 코드에서 한 가지 이슈를 제기해 볼께요.
위 코드에서 getHourMessage() 메소드가 getDateMessage()에서만 사용되고 있는데요. 이 메소드를 public 접근 제한으로 구현해야 될까요? 만약 private으로 구현할 경우 테스트는 어떻게 할까요? private의 경우 별도의 프레임워크를 사용해 단위 테스트를 해야 되기 때문에 이런 불편함을 덜기 위해 default 접근 제한으로 바꿔야 한다. 이와 같이 단위 테스트를 위해 method의 접근 제한을 변경해야 되는 상황이 발생해야 되는데요. 이와 관련해 어떻게 생각하시나요?
메소드를 분리해 테스트하는 것은 좋은데 뭔가 좀 찜찜한 구석이 있습니다. 이 문제를 어떻게 풀어가는 것이 좋을까요?
자바지기님의 질문을 듣고 생각해보았습니다.
1. getHourMessage() 메소드가 getDateMessage()에서만 사용되고 있는데 public 접근제한으로 구현해야 될까요?
서로 다른 이름으로 메소드를 분리하는 것보다 해당 메소드를 오버로딩해서 public 접근지정자로 제공하는 것이 더 좋을 것 같네요.
동일한 인터페이스를 제공함으로써 인자 유무에 따라 결과를 반환하게끔 처리하는게 깔끔할 것 같습니다.
2. 만약 private으로 구현할 경우 테스트는 어떻게 할까요?
private method를 테스트하는 방법으로 다양한 방법이 있는 걸로 알고 있는데요.
1) 직접 reflection을 이용하여 private 메소드의 접근 지정자를 변경하여 호출하기 2) 1번을 구현해 놓은 프레임워크들 사용하기 (powermock, dp4j 등) 3) production class에 inner class를 생성하여 private method 에 간적접으로 접근하기
몇몇개들은 직접 써보았는데 코드의 가독성면이나 유지보수 측면에서 깔끔하지 못하여 사용하고 있지는 않네요. (제가 잘 못사용하는 걸수도^^;)
개인적인 습관으로 private method들은 public 메소드를 통해서 간접적으로 테스트하거나 간단한데도 확신이 서지 않는 그런? 코드들은 public 으로 변경하여 테스트 후 private 으로 바꾸어 놓는데요. 테스트코드에도 주석을 달아 놓습니다만 사실 옳은 방법은 아닌 것 같습니다.^^;
이번 Question 같은 경우처럼 예외적인 사항에서는 취향에 따라 위 private 방법들을 이용하는 것도 방법이 될 것 같네요.
코드의 가독성을 높이고 해당 메소드가 하는 일을 명확히 하기 위해서 extract method 등으로 자주 리팩토링하게 되는데요. 테스트에 관련해서 항상 요런 문제에 부딪히는 것 같습니다.
업무중에 답글을 달아서 의도하신 질문이 맞는지 모르겠네요.^^; 무심코 지나갈만 것들인데 많은 생각을 하게끔 해주네요. 감사합니다.
첫번째 메소드에 return 이 빠져 있어요.
말씀하신 부분에 집중해서 의견을 덧붙여보겠습니다.
일단 getDateMessage(int) 가 public 으로 가지기에는 너무 역할이 적고 단순합니다. 마치 DAO단에서 DB 커넥션을 테스트 하는 상황이라고 할까요? 굳이 테스트 한다면 할 수 있지만 왜 해야하는지 어색한 상황인 것 같습니다. 12 이라는 바운더리 컨디션을 체크하기에는 int라는 값이 예외 케이스에 대한 가능성 자체를 줄여버리는 것 같습니다.
호응은 없지만 제가 밀고 있는 방식을 다시 꺼내오면
두번째 메소드만으로도 존재 이유가 충분할 것 같습니다. 바로 public으로 공개를 해도 되지만, 추후 공개 가능한 후보로 생각해볼 수도 있을 것 같습니다. 현재 시간이 아닌 특정 시간에 대한 표기 시 사용할 수도 있고, 국제화를 통해 오전, 오후가 아닌 AM, PM 을 표시해줄 수도 있는 가능성이 있는 객체라고 봅니다.
자바 안한지 2년째라 잘되어 있는지 모르겠습니다.
자바용 REPL 도 붙여주세요~
의견을 남기기 위해서는 SLiPP 계정이 필요합니다.
안심하세요! 회원가입/로그인 후에도 작성하시던 내용은 안전하게 보존됩니다.
SLiPP 계정으로 로그인하세요.
또는, SNS 계정으로 로그인하세요.