본문 바로가기
Spring/테스트

[테스트 주도 개발 시작하기] Chap02 TDD

by compile-this 2024. 8. 29.

 

TDD란?

기능을 검증하는 테스트 코드를 먼저 작성하고 테스트를 통과시키기 위해 개발을 진행하는 개발 방법

 

 

TDD 예제 코드

요구 기능

  • 암호검사기

요구 사항

  • 문자열을 검사해서 아래 규칙 모두를 충족하면 '강함', 두 가지만 충족하면 '보통', 한가지만 충족하면 '약함'으로 구분
  • 암호 검사 규칙
    - 길이가 8글자 이상
     - 0부터 9 사이의 숫자를 포함
     - 대문자 포함

참고: 테스트 케이스가 많아 중간중간 생략하고 TDD의 흐름을 볼 수 있는 코드 위주로 작성했다.

 

 

첫 번째 테스트: 모든 규칙을 충족하는 경우

  • 첫번째 테스트를 선택할 때에는 가장 쉽거나 가장 예외적인 상황을 선택해야 후에 진행과정이 수월하다.
  • 암호 검사기에서 가장 쉽거나 예외적인 상황
    • 모든 규칙을 충족하는 경우 (조건 검사 코드 없이 ‘강함’에 해당하는 값을 리턴하면 테스트 통과 가능)
    • 모든 조건을 충족하지 않는 경우 (각 조건을 검사하는 코드를 모두 구현해야 함, 구현양이 많음)


 1. 첫 번째 테스트 메서드 작성 (Red)

public class PasswordStrengthMeterTest {
    @Test
    void meetsAllCriteria_Then_Strong() {
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        PasswordStrength result = meter.meter("ab12!@AB");
        assertEquals(PasswordStrength.STRONG, result);
    }
}
  • PasswordStrengthMeter와 PasswordStrength가 존재하지 않기 때문에 컴파일 에러 발생

2. 컴파일 에러가 발생하지 않도록 구현 클래스 추가 (Red -> Green)

// src/test/java/chap02/PasswordStrength.java
public enum PasswordStrength {
    STRONG
}

// ---- 

// src/test/java/chap02/PasswordStrengthMeter.java
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return null;
    }
}
  • 테스트 메서드를 통과시킬 만큼의 코드만(컴파일 에러를 없애기 위한 최소 단위의 코드) 작성
  • 패키지를 잘 보면 test패키지 하위에 클래스 추가 
    • 아직 미구현된 클래스이기 때문에 배포에 포함되지 않도록 test패키지 하위에 추가

 

3. 테스트를 통과시키기 위한 구현 코드 수정 (Red -> Green)

// src/test/java/chap02/PasswordStrengthMeter.java
public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return PasswordStrength.STRONG;
    }
}
  • 컴파일 에러를 없애도 테스트에 통과하지 않기 때문에 테스트를 통과시킬 만큼의 코드를 작성

 

세 번째 테스트: 숫자를 포함하지 않고 나머지 조건은 충족하는 경우

 1. 테스트 메서드 작성 (Red)

@Test
void meetsOtherCriteria_except_for_number_Then_Normal() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter("ab!@ABqwer");
    assertEquals(PasswordStrength.NORMAL, result);
}

 

 

2. 테스트를 통과시키기 위한 구현 코드 수정 (Red -> Green)

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        if(s.length() < 8) {
            return PasswordStrength.NORMAL;
        }
        boolean containsNum = false;
        for(char ch : s.toCharArray()) {
            if(ch >= '0' && ch <= '9') {
                containsNum = true;
                break;
            }
        }
        if(!containsNum) {
            return PasswordStrength.NORMAL;
        }
        return PasswordStrength.STRONG;
    }
}

 

 

3. 구현 코드 리팩토링 (Refactor)

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        if(s.length() < 8) {
            return PasswordStrength.NORMAL;
        }
        boolean containsNum = this.meetsContainingNumberCriteria(s);
        if(!containsNum) {
            return PasswordStrength.NORMAL;
        }
        return PasswordStrength.STRONG;
    }

    private boolean meetsContainingNumberCriteria(String s) {
        for(char ch : s.toCharArray()) {
            if(ch >= '0' && ch <= '9') {
                return true;
            }
        }
        return false;
    }
}
  • 숫자를 포함하는지 여부를 검사하는 코드를 메서드로 분리

4. 테스트 코드 정리 (Refactor)

@Test
void 메서드 이름() {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();
    PasswordStrength result = meter.meter(암호);
    assertEquals(PasswordStrength.값, result);
}
  • 테스트 코드를 보면 위와 같은 형태가 반복된다. 

  • 리팩토링 후 
public class PasswordStrengthMeterTest {
    PasswordStrengthMeter meter = new PasswordStrengthMeter();

    private void assertStrength(String password, PasswordStrength expStr) {
        PasswordStrength result = meter.meter(password);
        assertEquals(expStr, result);
    }
    
    @Test
    void meetsAllCriteria_Then_Strong() {
        this.assertStrength("ab12!@AB", PasswordStrength.STRONG);
        this.assertStrength("abc1!Add", PasswordStrength.STRONG);
    }
}

 

 

모든 테스트 통과 완료 후

  • 모든 테스트 메서드에 대해서 구현코드를 작성하고 테스트를 통과하면 src/test/java 소스폴더에 위치한 구현 클래스를 src/main/java 소스폴더로 이동한다.

 

 

TDD 흐름

Red - Green - Refactor

지금까지 작성한 코드 순서 번호 옆에 Red, Green, Refactor를 작성해놓았다. 

각 단계에 대한 의미는 아래와 같다.

  • Red: 실패하는 테스트
  • Green: 성공한 테스트
  • Refactor: 리팩토링 과정

TDD는 다음과 같은 흐름으로 진행된다. (Red)먼저 기능을 검증하는 테스트를 작성한다. (Green)그 다음, 테스트를 통과할 만큼만 코드를 작성한다. (Refactor)마지막으로 개선할 코드가 있으면 리팩토링을 한다. 
이 과정을 반복하면서 점진적으로 기능을 완성해 나가는 것이 전형적인 TDD의 흐름이다.

 

 

TDD를 사용함으로써 얻을 수 있는 이점

  • 위에서 본 것 처럼  개발 과정에서 지속적으로 리팩토링을 하므로 코드 품질이 급격히 나빠지지 않게 막아 주는 효과가 있다.
  • 수정에 대한 피드백이 빠르다.
    • 코드를 추가하거나 기존 코드를 수정하면 테스트를 돌려서 해당 코드가 올바른지 바로 확인할 수 있다.
    • 구현 후 테스트 코드를 작성한다면 이런 이점을 누리기 힘들다.

위에까지가 책에 기재된 TDD의 이점(장점)들이다.
아직 본격적으로 현업에 적용해보지는 않았지만, 내가 생각하는 또다른 TDD의 장점은 개발 시간이 단축될 수 있다는 점이다.
물론 구현 후 테스트코드를 작성한다는 전제가 있어야한다. 구현한 뒤에 테스트 코드를 작성하게 되면 구현 코드에 맞는 테스트를 작성해야 되기 때문에 테스트 코드 작성이 또 다른 구현이 될 수 있을 것 같다. 반면에 TDD같은 경우에는 테스트 코드를 작성하면서 검증과 함께 구현을 같이하기에 이런 문제가 좀 덜하지 않을까 하는 의견이다.