객제지향, TDD, 클린코드/클린코드

깨끗한 테스트 코드 유지하기

ummchicken 2023. 3. 17. 18:31

나 닮음

 

 

오늘은 단위 테스트에 대해 학습을 해볼 것이다.

 

테스트코드를 짜는 것도 기능 구현과 같이 또 하나의 코드를 작성하는 것이므로 어렵지만, 

테스트코드의 단위를 설정하는 것도 

초보 개발자인 나에겐 어렵다.

 

 

 

테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화한다.
그러므로 테스트 코드는 깨끗하게 관리하자.

 

 

 


 

✔️ TDD 법칙 세 가지

  1. 첫째 법칙: 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  2. 둘째 법칙: 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 셋째 법칙: 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

 

 

예를 들어, 숫자 1 ~ 9까지 검증하는 메서드를 짠다고 가정해보자.

 

먼저 테스트 코드를 짠다.

@Test
void 숫자_1_9_검증() {
    assertThat(ValidationUtils.validNo(9)).isTrue();
    assertThat(ValidationUtils.validNo(1)).isTrue();
    assertThat(ValidationUtils.validNo(0)).isFalse();
    assertThat(ValidationUtils.validNo(10)).isFalse();
}
  • 이렇게 하면 컴파일 에러가 날 것이다.

 

 

컴파일 에러가 나지 않게, ValidationUtils라는 클래스를 생성한다.

 

 

[ValidationUtils]

public class ValidationUtils {

}
  • ValidationUtils 클래스를 만들어도, 여전히 컴파일 에러가 날 것이다.
  • ValidationUtils의 validNo 메서드가 없기 때문이다.

 

 

ValidationUtils의 validNo 메서드를 생성한다.

 

 

public class ValidationUtils {

    public static boolean validNo(int no) {
        return no >= 1 && no <= 9;
    }
}
  • 테스트 코드에서 작성했다시피 boolean을 반환하는 validNo 메서드이다.
  • 숫자를 매개변수로 받아 참/거짓 여부를 판단한다.

 

 

🚨 하드코딩을 피하기 위해 숫자를 상수로 지정해보자.

리팩토링.

public class ValidationUtils {

    public static final int MIN_NO = 1;
    public static final int MAX_NO = 9;

    public static boolean validNo(int no) {
        return no >= MIN_NO && no <= MAX_NO;
    }
}

 

 

여기까지 간단한 예제였다.

 

 

 

 

✔️ 깨끗한 테스트 코드

💡 깨끗한 테스트 코드를 만들려면 가독성이 중요하다.

 

 

 

🤔 그럼 가독성을 높이려면?

  • 명료성
  • 단순성
  • 풍부한 표현력

 

 

테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.

 

 

 

 

✔️ 테스트 당 개념 하나

💡 JUnit으로 테스트 코드를 짤 떄는 함수마다 한 개념만 테스트하라.

 

 

이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피한다.

 

 

다음은 바람직하지 못한 테스트 함수이다.

@Test
void testAddMonths() {
    SerialDate d1 = SerialDate.createInstance(31, 5, 2004);

    SerialDate d2 = SerialDate.addMonths(1, d1);
    assertEquals(30, d2.getDayOfMonth());
    assertEquals(6, d2.getMonth());
    assertEquals(2004, d2.getYYYY());

    SerialDate d3 = SerialDate.addMonths(2, d1);
    assertEquals(31, d3.getDayOfMonth());
    assertEquals(7, d3.getMonth());
    assertEquals(2004, d3.getYYYY());

    SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
    assertEquals(30, d4.getDayOfMonth());
    assertEquals(7, d4.getMonth());
    assertEquals(2004, d4.getYYYY());
}
  • addMonths() 메서드를 테스트하는 장황한 코드이다.
  • 독자적인 개념 세 개를 테스트하므로, 독자적인 테스트 세 개로 쪼개야 마땅하다.
  • 새 개념을 한 함수로 몰아넣으면, 독자가 각 절이 거기에 존재하는 이유와 각 절이 테스트하는 개념을 모두 이해해야 한다.

 

 

이것을 셋으로 분리한 테스트 함수는 각각 다음 기능을 수행한다.

  • (5월처럼) 31일로 끝나는 달의 마지막 날짜가 주어지는 경우
    1. (6월처럼) 30일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되어서는 안 된다.
    2. 두 달을 더하면 그리고 두 번쨰 달이 31일로 끝나면 날짜는 31일이 되어야 한다.

 

  • (6월처럼) 30일로 끝나는 달의 마지막 날짜가 주어지는 경우
    1. 31일로 끝나는 한 달을 더하면 날짜는 30일이 되어야지 31일이 되면 안 된다.

 

 

 

위의 코드는 각 절에 assert 문이 여럿이라는 사실이 문제가 아니다.

한 테스트 함수에서 여러 개념을 테스트한다는 사실이 문제다.

 

 

💡 그러므로 가장 좋은 규칙은 

"개념 당 assert 문 수를 최소로 줄여라"와 

"테스트 함수 하나는 개념 하나만 테스트하라"이다.

 

 

 

 


 

✔️ F.I.R.S.T

깨끗한 테스트는 다음 다섯 가지 규칙을 따르는데, 각 규칙의 첫 글자가 FIRST이다.

 

 

1. 빠르게(Fast)

테스트는 빨라야 한다.

 

테스트는 빨리 돌아야 한다는 말이다.

 

테스트가 느리면 자주 돌릴 엄두를 못 내고, 

그러면 초반에 문제를 찾아내고 고치지 못한다.

 

 

 

2. 독립적으로(Independent)

각 테스트는 서로 의존하면 안 된다.

 

한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다.

 

각 테스트는 독립적으로, 그리고 

어떤 순서로 실행해도 괜찮아야 한다.

 

 

 

 

3.  반복가능하게(Repeatable)

테스트는 어떤 환경에서도 반복 가능해야 한다.

 

테스트가 돌아가지 않는 환경이 하나라도 있아면, 

테스트가 실패한 이유를 둘러댈 변명이 생긴다!

 

 

 

4. 자가검증하는(Self-Validating)

테스트는 bool 값으로 결과를 내야 한다.
즉, 성공 아니면 실패다.

 

예를 들어, 통과 여부를 알려고 로그 파일 등을 읽게 만들어서는 안 된다.

 

 

 

5. 적시에(Timely)

테스트는 적시에 작성해야 한다.
💡 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

 

실제 코드를 구현한 다음에 테스트 코드를 만들면, 

실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다.

 

어떤 실제 코드는 테스트하기 너무 어렵다고 판명날지 모른다.

 

테스트가 불가능하도록 실제 코드를 설계할지도 모른다.

(오...)

 

 

 

 


 

 

인상 깊은 문장이 있었다.

테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다.

 

 

 

 

 

 

 

 

 

 

참고: Clean Code 클린 코드 애자일 소프트웨어 장인 정신 / 로버트 C. 마틴 저