본문 바로가기

Spring/Test

테스트 주도 개발 시작하기 - TDD 시작, TDD 암호 검사기 1

구현할 기능: 암호 검사기

 

검사 규칙

  1. 길이가 8자 이상
  2. 0 ~9 숫자를 포함
  3. 대문자 포함

 

암호 강도

  1. 3개 규칙 모두 충족 시 암호는 강함
  2. 2개 규칙 충족 시 암호는 보통
  3. 1개 규칙 충족 시 암호는 약함

 

1. 첫 번째 테스트

가장 쉽거나 가장 예외적인 상황을 선택해야 함

모든 규칙을 충족하는 경우, 모든 조건을 충족하지 않은 경우 중 모든 규칙을 충족하는 경우를 먼저 테스트

왜? 모든 규칙을 충족시키지 않는 경우 테스트를 통과시키려면 각 조건을 검사하는 코드를 모두 구현해야 하므로 한 번에 만들어야 할 코드가 많아지고 시간이 길어짐 -> 사실상 구현을 다 하고 테스트 하는 방식과 큰 차별점이 없음

 

1) 테스트 코드 구현

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PasswordStrengthMeterTest {
    @Test
    @DisplayName("비밀번호 강도 강함")
    void meetsAllCriteria_Then_Strong() {
        PasswordStrengthMeter meter = new PasswordStrengthMeter();
        
        PasswordStrength result = meter.meter("ab12!!@AB"); // 리턴 타입 고민 후 적절한 타입을 사용해야 함
        assertEquals(PasswordStrength.STRONG, result);
        
        PasswordStrength result2 = meter.meter("abc12!dDd");
        assertEquals(PasswordStrength.STRONG, result2);

    }


}

 

-> PasswordStrengthMeter 타입과 PasswordStrength 타입이 없으므로 컴파일 에러 발생

 

 2) 컴파일 에러를 없애기 위해 PasswordStrengthMeter 클래스 생성 

 

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        return PasswordStrength.STRONG;

 

3) 컴파일 에러를 없애기 위해 PasswordStrength 클래스 생성 

public enum PasswordStrength {
    STRONG
}

-> 컴파일 에러가 없으므로 테스트 실행 가능, STRONG을 반환 하므로 테스트 통과 성공

 

 

2. 두 번째 테스트

길이가 8글자 이상을 충족시키지 못하고, 다른 2개 조건은 충족하는 경우 

 

1) 테스트 코드 추가

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PasswordStrengthMeterTest {
// 생략

    @Test
    @DisplayName("비밀번호 강도 보통 - 길이 8자 미만")
    void meetsOtherCriteria_except_for_Then_Normal() {
        PasswordStrengthMeter meter = new PasswordStrengthMeter();

        PasswordStrength result = meter.meter("ab12!@A");
        assertEquals(PasswordStrength.NORMAL, result);

        PasswordStrength result2 = meter.meter("ac@wS2!");
        assertEquals(PasswordStrength.NORMAL, result2);

    }

}

-> PasswordStrength 타입에 NORMAL이 없으므로 컴파일 에러 발생

 

 

2) PasswordStrength 클래스에 NORMAL 추가

public enum PasswordStrength {
    STRONG,
    NORMAL
}

-> 추가 후 테스트 코드를 실행하면 테스트 실패

 

 

3) 테스트 코드가 모두 통과 될 수 있도록 PasswordStrengthMeter 클래스에 코드 추가

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        if(s.length() < 8) {
            return PasswordStrength.NORMAL;
        }
        return PasswordStrength.STRONG;
    }
}

-> 코드를 추가하면 테스트 통과 성공

 

 

3. 세 번째 테스트

숫자를 포함하는 조건을 충족 시키지 못하고, 다른 2개 조건은 충족하는 경우 

 

1) 숫자를 포함하는 조건을 충족시키지 못하는 테스트 코드 추가

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PasswordStrengthMeterTest {
// 생략
    
    @Test
    @DisplayName("비밀번호 강도 보통 - 숫자 미포함")
    void meetsAllOtherCriteria_except_for_number_Then_Normal() {
        PasswordStrengthMeter meter = new PasswordStrengthMeter();

        PasswordStrength result = meter.meter("abcd!!@A");
        assertEquals(PasswordStrength.NORMAL, result);

        PasswordStrength result2 = meter.meter("ac@wSfe!e");
        assertEquals(PasswordStrength.NORMAL, result2);
    }
    
    

}

-> 길이는 8자 이상이기 때문에 STRONG이 결과값이기 때문에 테스트 코드 실패

 

 

2) 테스트 코드가 모두 통과 될 수 있도록 PasswordStrengthMeter 클래스에 코드 추가

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;
    }
}

 

 

4. 코드 리팩터링

코드가 다소 길어지고 가독성이 좋지 않으므로 코드를 리팩터링함

 

1) PasswordStrengthMeter 클래스 코드 리팩터링

public class PasswordStrengthMeter {
    public PasswordStrength meter(String s) {
        if(s.length() < 8) {
            return PasswordStrength.NORMAL;
        }
        
        boolean containsNum = 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;
    }
    
}

 

 

5. 테스트 코드 리팩터링

- 테스트 코드도 코드이기 때문에 유지보수 대상

- 테스트 메소드에서 발생하는 중복을 제거하거나 의미가 드러나도록 코드 수정

- 코드 수정 후, 테스트 재 실행 : 코드 수정으로 테스트가 깨지지 않았는 지 확인

 

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PasswordStrengthMeterTest {
    private PasswordStrengthMeter meter = new PasswordStrengthMeter(); // 중복되는 코드, 필드에서 생성하도록 수정

    private void assertStrength(String password, PasswordStrength expStr) { // 중복되는 코드, 메소드를 이용해서 제거
        PasswordStrength result = meter.meter(password);
        assertEquals(expStr, result);
    }

    @Test
    @DisplayName("비밀번호 강도 강함")
    void meetsAllCriteria_Then_Strong() {
        assertStrength("ab12!!@AB", PasswordStrength.STRONG);
        assertStrength("abc12!dDd", PasswordStrength.STRONG);
    }

    @Test
    @DisplayName("비밀번호 강도 보통 - 길이 8자 미만")
    void meetsOtherCriteria_except_for_Then_Normal() {
        assertStrength("ab12!@A", PasswordStrength.NORMAL);
        assertStrength("ac@wS2!", PasswordStrength.NORMAL);
    }

    @Test
    @DisplayName("비밀번호 강도 보통 - 숫자 미포함")
    void meetsAllOtherCriteria_except_for_number_Then_Normal() {
        assertStrength("abcd!!@A", PasswordStrength.NORMAL);
        assertStrength("ac@wSfe!e", PasswordStrength.NORMAL);
    }


}