이해하기 쉬운..가독성 좋은.. Date 테스트 작성

2014-03-30 04:23

관리해 주 자나~

아직 현실에서는(!?) Product Code 에 비해 Test Code 는 덜 관리 되는 것 같습니다. 저는 Test Code 또한 관리대상이며 어찌보면 Product Code 보다 더 가독성이 높아야 한다고 생각합니다. 가독성이 좋지 않는 Test Code 는 Test 를 방해하고 결국 Product 의 품질을 낮추니까요.

주말 여기 저기 코드를 보다 고치고 싶은 코드가 있어 테스트를 열다 한참을 들여다보게 만든 테스트 가 있었습니다.

이제 어떤 특정한 상황(쉴드..) 에 맞게 가독성에 대해서 썰을 풀어보겠습니다.

Date 테스트인데 한참을 뚤어지게 본 결과 인풋으로 Date 를 받고 아웃풋으로 변환하여 Date를 내보내는 것을 테스트하는 코드였습니다.

약간 각색한 비슷한 코드가 다음과 같습니다. 각색하며 한글사용을 해보았습니다. 테스트가 눈에 잘 들어 오시나요?

    @Test
    public void 두시간뒤로시간을돌린다(){
        Calendar cal = Calendar.getInstance();
        cal.set(YEAR, 2014);
        cal.set(MONTH, MARCH);
        cal.set(DATE, 1);
        cal.set(HOUR_OF_DAY, 5);
        cal.set(MINUTE, 0);
        cal.set(SECOND, 0);
        cal.set(MILLISECOND, 0);


        Calendar cal2 = Calendar.getInstance();
        cal2.setTime(sut.두시간_뒤로_돌린다(cal.getTime()));


        assertThat(cal2.get(MONTH), is(MARCH));
        assertThat(cal2.get(DATE), is(1));
        assertThat(cal2.get(HOUR_OF_DAY), is(3));
        assertThat(cal2.get(MINUTE), is(0));
        assertThat(cal2.get(SECOND), is(0));
        assertThat(cal2.get(MILLISECOND), is(0));


        // 한글메서드가 두시간뒤라고하니 그렇다 치고(한글메서드를 왜 사용했냐는 둥 딴지는 걸지 말아주세요 ㅜㅜ) 테스트 Actual Data 와 Expected Data 의 비교 단정문이 눈에 들어오는가?
    }

그나마 한글화를 통해서 일단 약간에(??) 가독성에 좋은 영향을 주었는지 모르겠으나 사실 이 테스트코드를 보고 단정문이 무엇을 단정하고 있는지 눈에 들어오지 않았습니다. 일단 저에 눈에는 말이죠…

그래서 파라메터를 더 또렸하게 만들어 보았습니다. 한글을 이용해 의미를 더 또렸하게 하려고 애를 써보았습니다. 한국인으로써 모국어를 테스크코드에 적용해보니 많은 부분 또렸하게 드러나는 부분이 많은 것 같습니다. (적어도 테스트코드에 작명을 어떻게 해야하는지 고민하는 시간은 줄었습니다.)

    @Test
    public void 세시간뒤로시간을돌린다() throws Exception{
        Calendar cal2 = Calendar.getInstance();
        cal2.setTime(sut.세시간_뒤로_돌린다(_2014년_3월_1일_오전_05시()));


        assertThat(cal2.get(MONTH), is(MARCH));
        assertThat(cal2.get(DATE), is(1));
        assertThat(cal2.get(HOUR_OF_DAY), is(2));
        assertThat(cal2.get(MINUTE), is(0));
        assertThat(cal2.get(SECOND), is(0));
        assertThat(cal2.get(MILLISECOND), is(0));
    }


    private Date _2014년_3월_1일_오전_05시() {
        Calendar cal = Calendar.getInstance();
        cal.set(YEAR, 2014);
        cal.set(MONTH, MARCH);
        cal.set(DATE, 1);
        cal.set(HOUR_OF_DAY, 5);
        cal.set(MINUTE, 0);
        cal.set(SECOND, 0);
        cal.set(MILLISECOND, 0);
        return cal.getTime();
    }

딴에는 2014년 3월1일 오전 5시 를 세시간 뒤로 돌릴거라는 상황에 대해서는 눈에 들어오네요.

단정문에서 Calendar 를 이용하여 하나하나 비교하는 것 보다 더 명확하게 테스트를 파악 할 수 있도록 문자열을 이용해보았습니다. (다들 문자열로 비교하시죠?? 의외로 생각 못하시기도 하더군요…)

  @Test
  public void 네시간뒤로시간을돌린다() throws Exception{
     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
     assertThat(sdf.format(_2014년_3월_1일_오전_05시()),is("2014-03-01 05:00"));


     assertThat(sdf.format(sut.네시간_뒤로_돌린다(_2014년_3월_1일_오전_05시())), is("2014-03-01 01:00"));
  }

한글을 이용해 테스트에서 사용될 여러 Fixture 들을 미리 아래 처럼 멤버로 만들어 둘 수 도 있습니다.

 SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm");
 Date _2014년_3월_1일_5시정각 = 시간(2014, 2, 1, 5, 0);
 Date _2014년_3월_2일_5시정각 = 시간(2014, 2, 2, 5, 0);


 @Test
 public void 다섯시간뒤로시간을돌린다() throws Exception{
   assertThat(SDF.format(sut.다섯시간_뒤로_돌린다(_2014년_3월_1일_5시정각)),is("2014-03-01 00:00"));
 }


 private Date 시간(int year,int month,int date, int hour, int minite) {
   Calendar cal = Calendar.getInstance();
   cal.set(YEAR, year);
   cal.set(MONTH, month);
   cal.set(DATE, date);
   cal.set(HOUR_OF_DAY, hour);
   cal.set(MINUTE, minite);
   cal.set(SECOND, 0);
   cal.set(MILLISECOND, 0);
   return cal.getTime();
 }

아직도 SimpleDateFormat 등 장황해 보기도 합니다.(개인적인 취향일 수 있습니다..) 매번 SDF.form(……) 작성하는 것도 번거롭기도 하고... 그래서 단정문 메서드를 통해 매번 작성하는 부분에 대해서 제거해보았습니다.

 @Test
 public void 시간을돌린다() throws Exception{
   assertEquals("2014-03-01 06:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, -1));
   assertEquals("2014-03-01 05:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 0));
   assertEquals("2014-03-01 04:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 1));
   assertEquals("2014-03-01 03:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 2));
   assertEquals("2014-03-01 02:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 3));
   assertEquals("2014-03-01 01:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 4));
   assertEquals("2014-03-01 00:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 5));
   assertEquals("2014-02-28 23:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 6));
}


private void assertEquals(String expected, Date date) {
    assertThat(SDF.format(date),is(expected));
}

“두시간뒤로시간을돌린다” 에서 테스트 는 약 스므줄을 차지 하고 있고 “시간을 돌린다”테스트는 8줄입니다. 어떤가요 처음 “두시간뒤로시간을돌린다” 테스트보다 가독성 면에서 좋아 졌나요 ? 후자가 더 많은 테스트를 담고 있다는 점도 주목해주세요. 또한 테스트 추가 또한요…. “두시간..” 테스트 는 하나의 테스트인데 비해 “시간을 돌린다” 테스트는 많은 케이스를 담고 있습니다. 당근 쉬운 테스트 추가가 한목 하고 있습니다.

"요즘시대가 어떤시대인데 아직도 assertEquals 이가…??" 머 참고로 assertThat 을 사용하여 표현하고 싶은 마음이 들었습니다. org.hamcrest.Matcher 구현해 얼마든지 깔끔한 단정문을 통해 가독성을 높일 수 있습니다. 본 예 에서는 머 그리 달라 보이지는 않을거라 생각합니다.

@Test
public void 시간을돌린다2() throws Exception{
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,-1),is(formmated("2014-03-01 06:00","yyyy-MM-dd HH:mm")));
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,0),is(formmated("2014-03-01 05:00","yyyy-MM-dd HH:mm")));
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,1),is(formmated("2014-03-01 04:00","yyyy-MM-dd HH:mm")));
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,2),is(formmated("2014-03-01 03:00","yyyy-MM-dd HH:mm")));
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,3),is(formmated("2014-03-01 02:00","yyyy-MM-dd HH:mm")));
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,4),is(formmated("2014-03-01 01:00","yyyy-MM-dd HH:mm")));
 assertThat(sut.시간을_돌린다(_2014년_3월_1일_5시정각,5),is(formmated("2014-03-01 00:00","yyyy-MM-dd HH:mm")));
    }


    public static class DateFormatMatcher extends TypeSafeDiagnosingMatcher<Date>{


        private final SimpleDateFormat sdf;
        private final String expected;


        public DateFormatMatcher(String format, String expected) {
            this.sdf =new SimpleDateFormat(format);
            this.expected = expected;
        }


        @Override
        public void describeTo(Description description) {
            description.appendText(expected);
        }


        @Override
        protected boolean matchesSafely(Date item, Description mismatchDescription) {
            mismatchDescription.appendText(sdf.format(item));
            return sdf.format(item).equals(expected);
        }


        @Factory
        public static Matcher<Date> formmated(String expected,String format){
            return new DateFormatMatcher(format,expected);
        }


    }


분명 혹자는 “이렇게 까지 해야하는가?” 라고 물어 볼거라 생각합니다…

답변하자면 이렇게 해야한다. 라고 생각합니다. 꼭

물론 위에서 여러 단계를 보았는데 어느 수준에서 테스트 의도가 드러난다면 하드코어하게 할 필요는 없다고 생각합니다.

하지만 테스트코드는 관리되어져야 하고 제품코드와 같은 취급을 받아야 합니다. 흔하게 보여지듯 테스트코드를 처음 작성시에 또는 CI 가 돌리는 머 그런 요소로만 생각하고 무성의 하게 작성되어지는데 이부분이 더 많은 테스트를 작성을 방해하고 있는 요소 중에 하나라고 생각합니다.

그러므로 테스트코드는 깔끔하게 이해하기 좋은 가독성을 가져야 하지 않을까요??

기스트에 전체 소스를 올려두었습니다. https://gist.github.com/yangwansu/9860994

0개의 의견 from FB

4개의 의견 from SLiPP

2014-03-30 07:25

날짜

글의 전체적인 맥락에서는 찬성합니다. 테스트 코드의 가독성을 높이자. 테스트 코드를 명백하게 하자. 테스트 메서드명의 한글 사용 등..

그럼, 위의 태스트메서드명을 보면 메서드의 역활은 '입력된 날짜와사간을 2시간전의 날짜와 시간을 돌려준다'입니다. 맞나요? 맞다면, 첫번째, 테스트 코드에 대해 읽으면 첫느낌이 '과하다' 입니다. 입력된 날짜의 2시간전의 결과만이 아닌 다른 시간들 -2,-1,0,1,2,,,,등 2시간전의 날짜이외에도 많은걸 테스트 합니다.(마치 날짜의 시간계산 같은 생각이 더 듭니다. 두번째, 날짜 경계선에 대한 테스트는 빠진것도 같습니다. 예) '2014/03/03 01:00'의 2시간 전은? - 시간 검토 '2014/03/03 01:00'의 2시간 전은? - 일 변경 검토 '2014/03/01 01:00'의 2시간 전은? - 월 변경 검토 '2014/01/01 01:00'의 2시간 전은? - 년 변경 검토 '2012/03/01 01:00'의 2시간 전은? - 윤년 변경 검토 저의 경험에서는 시간계산시, 주로 날짜경계에 대한 부분에서 오류가 많았거든요.

테스트 코드를 만들때, 무엇을 테스트하는지? 빠진 테스트가 없는지? 왜, 하는지?

처음부터, 생각을 하도록 하고 특히나, 주어진 것에만 한하여 테스트에만 '집중'을 하도록 만드는것에 더, 더, 더, 더 큰 매력이 있다 생각합니다. (일명, 삼천포 빠지기 안티패턴 ^^)

하윤아빠님의 한글명 사용에는 저 또한 적극적으로 찬성합니다. 주변에도 한글명을 사용하도록 권장합니다. ^^

감사합니다.

2014-03-30 08:49

@움직이는 답변감사드립니다. ^^

주신 답변중에 "과하다" 라는 부분은 어느 부분인지 궁금합니다. 경계에 대한 테스트 부분은 대찬성합니다.

핵심은 "두시간뒤로.." 에 대한 테스트 보다 "시간을 돌린다" 의 테스트가 테스트 작성시 말씀하신 여러부분(윤년, 등) 애 대해서 작성이 유리하고 추후 독자에 대한 배려가 많다는 점입니다.

"시간을돌린다" 테스트는 -1 , 윤년에 대한 테스트가 "두시간뒤로.." 테스트 방식의 작성보다 쉽게 들어갈 수 있습니다.

{

 @Test
 public void 시간을돌린다() throws Exception{
   assertEquals("2014-03-01 06:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, -1));
   assertEquals("2014-03-01 05:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 0));
   assertEquals("2014-03-01 04:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 1));
   assertEquals("2014-03-01 03:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 2));
   assertEquals("2014-03-01 02:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 3));
   assertEquals("2014-03-01 01:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 4));
   assertEquals("2014-03-01 00:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 5));
   assertEquals("2014-02-28 23:00", sut.시간을_돌린다(_2014년_3월_1일_5시정각, 6));
}
    @Test
    public void 두시간뒤로시간을돌린다(){
        Calendar cal = Calendar.getInstance();
        cal.set(YEAR, 2014);
        cal.set(MONTH, MARCH);
        cal.set(DATE, 1);
        cal.set(HOUR_OF_DAY, 5);
        cal.set(MINUTE, 0);
        cal.set(SECOND, 0);
        cal.set(MILLISECOND, 0);


        Calendar cal2 = Calendar.getInstance();
        cal2.setTime(sut.두시간_뒤로_돌린다(cal.getTime()));


        assertThat(cal2.get(MONTH), is(MARCH));
        assertThat(cal2.get(DATE), is(1));
        assertThat(cal2.get(HOUR_OF_DAY), is(3));
        assertThat(cal2.get(MINUTE), is(0));
        assertThat(cal2.get(SECOND), is(0));
        assertThat(cal2.get(MILLISECOND), is(0));


        // 한글메서드가 두시간뒤라고하니 그렇다 치고(한글메서드를 왜 사용했냐는 둥 딴지는 걸지 말아주세요 ㅜㅜ) 테스트 Actual Data 와 Expected Data 의 비교 단정문이 눈에 들어오는가?
    }
2014-03-31 20:15

'과하다' 란 것은 예제의 테스트가 "2시간 뒤로 돌리기" 란 부분입니다. 2시간뒤로돌리기 인데, -1,0,1,2,3,4,5 이렇게 테스트를 하기에 2시간에뒤로돌리기에 한한 테스트가 아닌, 시간계산하는 걸로 느껴져서 '과하다' 라는 표현을 한것입니다. 그럼, 이만

2014-04-01 13:32

@움직이는 오해 하신듯 하네요

"2시간 뒤돌리기" 와 "시간을.." 테스트 를 보시면

"2시간 뒤돌리기" 는 1,0,1,2,3,4,5 에 대한 테스트를 하지 않습니다. 타임머신.2시간뒤돌리기() 는 메서드 시그니쳐에 파라메터를 가지지 않습니다. ...

말씀하신 시간 계산하는것은 "시간을 .." 요테스트가 하고 있지요..

글이 가독성이 떨어지나 봅니다... ㅜㅜ 글쓰기 좀 잘하고 싶네요 .

의견 추가하기

연관태그

← 목록으로