본문 바로가기

Spring/Test

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

1. 네 번째 테스트

값이 없는 경우

- 예외상황을 고려하지 않으면 비정상 작동

- 값이 없는 경우에 대해서 알맞은 동작이 되도록 테스트 해야함

- 값이 없는 경우에는 어떻게 작동해야 할 지 고민 필요, IllegalArgumentException 발생 시키거나, 유효하지 않은 암호를 의미하는 PasswordStrength.INVALID를 리턴하게 할 수 있음(코드는 후자의 방법으로 진행)

 

1) 테스트 코드 추가

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("비밀번호가 Null값으로 들어온 경우")
    void nullInput_Then_Invalid() {
        assertStrength(null, PasswordStrength.INVALID);
    }
}

->  PasswordStrength.INVALID가 없기 때문에 컴파일 에러 발생

 

2) PasswordStrength에 INVALID 추가

public enum PasswordStrength {
    STRONG,
    NORMAL,
    INVALID
}

 

-> NullPointerException 발생

 

 

3) PasswordStrengthMeter에 null 처리 구현 추가

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null)
            return PasswordStrength.INVALID;

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

}

 

4) 빈 문자열에 들어오는 예외 상황 테스트 추가 

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("비밀번호가 Null값으로 들어온 경우")
    void nullInput_Then_Invalid() {
        assertStrength(null, PasswordStrength.INVALID);
    }
    
    @Test
    @DisplayName("비밀번호가 빈문자열로 들어온 경우")
    void emptyInput_Then_Invalid() {
        assertStrength("", PasswordStrength.INVALID);
    }
}

 

-> 기대한 값은 INVALID이지만 NORMAL이 결과값

 

5) 테스트 코드를 통과시키기 위해 빈 문자열에 대한 구현 추가

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null || s.isEmpty()) // 빈 문자열에 대한 구현 추가 
            return PasswordStrength.INVALID; // PasswordStrengthMeter, null 처리 구현 추가

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

}

 

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 {
    private PasswordStrengthMeter meter = new PasswordStrengthMeter(); // 중복되는 코드, 필드에서 생성하도록 수정

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

// 생략

    @Test
    @DisplayName("비밀번호 강도 보통 - 대문자 미포함")
    void meetsOtherCriteria_except_for_Uppercase_Then_Normal() {
        assertStrength("abcd!!@d", PasswordStrength.NORMAL);
    }

}

-> 테스트 실패

 

2) 대문자를 포함하지 않은 경우에 대한 테스트를 통과시키기 위한 코드 작성

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null || s.isEmpty()) // 빈 문자열에 대한 구현 추가
            return PasswordStrength.INVALID; // PasswordStrengthMeter, null 처리 구현 추가

        if (s.length() < 8) {
            return PasswordStrength.NORMAL;
        }

        boolean containsNum = meetsContainingNumberCriteria(s);
        if (!containsNum) return PasswordStrength.NORMAL;
        
        // 대문자를 포함하지 않은 경우에 대한 테스트를 통과시키기 위한 코드 작성
        boolean containsUpp = false;
        for(char ch : s.toCharArray()) {
            if(Character.isUpperCase(ch)) {
                containsUpp = true;
                break;                
            }            
        }
        if(!containsUpp) 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;
    }

}

-> 테스트 성공

 

3) 코드 리팩토링

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null || s.isEmpty()) // 빈 문자열에 대한 구현 추가
            return PasswordStrength.INVALID; // PasswordStrengthMeter, null 처리 구현 추가

        if (s.length() < 8) { 
            return PasswordStrength.NORMAL;
        }

        // 숫자를 포함하지 않은 경우에 대한 테스트를 통과시키기 위한 코드 리팩토링
        boolean containsNum = meetsContainingNumberCriteria(s);
        if (!containsNum) return PasswordStrength.NORMAL;

        // 대문자를 포함하지 않은 경우에 대한 테스트를 통과시키기 위한 코드 리팩토링
        boolean containsUpp =  meetsContainingUppercaseCriteria(s);
        if(!containsUpp) 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;
    }

    // 대문자 확인 메소드 추출
    private boolean meetsContainingUppercaseCriteria(String s) {
        for(char ch : s.toCharArray()) {
            if(Character.isUpperCase(ch)) {
                return true;
            }
        }
        return false;
    }

}

-> 리팩토링 테스트 재 실행하기

 

3. 여섯 번째 테스트

길이가 8글자 이상인 조건만 충족하는 경우

 

1) 테스트 코드 작성

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("비밀번호 강도 약함 - 길이 8자 이상 조건만 충족")
    void meetsOnlyLengthCriteria_Then_Weak() {
        assertStrength("abcdefghi", PasswordStrength.WEAK);
    }

}

-> PasswordStrength.WEAK가 존재하지 않기 때문에 컴파일 에러 발생

 

2) 컴파일 에러를 없애기 위한 코드 수정

public enum PasswordStrength {
    STRONG,
    NORMAL,
    WEAK,
    INVALID
}

-> 코드 추가 후 테스트하면 테스트 실패, 테스트 결과가 WEAK이 나와야 하는데 NORMAL이 나옴

 

 

-> 테스트 통과를 위한 고민 하기

- 3개의 조건 중 8자 이상의 길이를 충족하고, 나머지 2개 조건은 충족하지 않을 때 WEAK을 리턴해야 함

- 3개의 조건을 모두 검사 후 그 결과에 따른 값을 리턴해야 함

- 위 상황을 고려했을 때 기존에 길이 검사 코드를 아래와 같이 수정

 

3) 코드 수정

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null || s.isEmpty()) // 빈 문자열에 대한 구현 추가
            return PasswordStrength.INVALID; // PasswordStrengthMeter, null 처리 구현 추가

//        아래 코드로 변경
//        if (s.length() < 8) {
//            return PasswordStrength.NORMAL;
//        }
        
        // 길이가 8자 미만일 경우에 대한 테스트를 통과시키기 위한 코드 리팩토링
        boolean lengthEnough = s.length() >= 8;
        if (!lengthEnough) return PasswordStrength.NORMAL;

        // 숫자를 포함하지 않은 경우에 대한 테스트를 통과시키기 위한 코드 리팩토링
        boolean containsNum = meetsContainingNumberCriteria(s);
        if (!containsNum) return PasswordStrength.NORMAL;

        // 대문자를 포함하지 않은 경우에 대한 테스트를 통과시키기 위한 코드 리팩토링
        boolean containsUpp =  meetsContainingUppercaseCriteria(s);
        if(!containsUpp) 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;
    }

    // 대문자 확인 메소드 추출
    private boolean meetsContainingUppercaseCriteria(String s) {
        for(char ch : s.toCharArray()) {
            if(Character.isUpperCase(ch)) {
                return true;
            }
        }
        return false;
    }

}

 

4) 코드 리팩토링

- if 절의 위치를 이동 시킴

왜? 크게 두 개의 로직(개별 규칙을 검사하는 로직, 규칙 검사 결과에 따른 암호 강도 계산 로직)으로 구분해서 모으기 위함

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null || s.isEmpty()) //  null, empty 처리를 위한 구현
            return PasswordStrength.INVALID; 
        
        // 길이가 8자 이상인 경우 true
        boolean lengthEnough = s.length() >= 8;
        // 0~9 범위의 숫자가 있는 경우 true
        boolean containsNum = meetsContainingNumberCriteria(s);
        // 대문자를 포함하는 경우 true
        boolean containsUpp =  meetsContainingUppercaseCriteria(s);

        if (!lengthEnough) return PasswordStrength.NORMAL;
        if (!containsNum) return PasswordStrength.NORMAL;
        if(!containsUpp) 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;
    }

    // 대문자 확인 메소드 추출
    private boolean meetsContainingUppercaseCriteria(String s) {
        for(char ch : s.toCharArray()) {
            if(Character.isUpperCase(ch)) {
                return true;
            }
        }
        return false;
    }

}

-> 아직 테스트가 성공되는 코드는 구현하지 않음, 테스트 성공을 위한 코드 구성을 갖춤

 

5) 길이가 8이상인 조건만 충족하는 경우를 통과시키기 위한 코드 추가

 

public class PasswordStrengthMeter {

    public PasswordStrength meter(String s) {
        if (s == null || s.isEmpty()) //  null, empty 처리를 위한 구현
            return PasswordStrength.INVALID;

        // 길이가 8자 이상인 경우 true
        boolean lengthEnough = s.length() >= 8;
        // 0~9 범위의 숫자가 있는 경우 true
        boolean containsNum = meetsContainingNumberCriteria(s);
        // 대문자를 포함하는 경우 true
        boolean containsUpp =  meetsContainingUppercaseCriteria(s);

        // 길이가 8이상인 조건만 충족하는 경우를 통과시키기 위한 코드 추가
        if (lengthEnough && !containsNum && !containsUpp) return PasswordStrength.WEAK;

        if (!lengthEnough) return PasswordStrength.NORMAL;
        if (!containsNum) return PasswordStrength.NORMAL;
        if (!containsUpp) 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;
    }

    // 대문자 확인 메소드 추출
    private boolean meetsContainingUppercaseCriteria(String s) {
        for(char ch : s.toCharArray()) {
            if(Character.isUpperCase(ch)) {
                return true;
            }
        }
        return false;
    }

}

-> 테스트 실행 통과