본문 바로가기
Programming/Java, Spring

[Java ] 테스트 코드를 작성하는 이유, TDD 코드 예시

by Renechoi 2022. 10. 23.

[Java ] by 홍종완님, 패스트캠퍼스 나만의 MVC 프레임워크 만들기 

 


 

테스트 코드를 작성하는 이유 

 

1. 문서화 역할

2. 코드에 결함을 발견하기 위함

3. 리팩토링시 안정성 확보

4. 테스트하기 쉬운 코드를 작성하다 보면 더 낮은 결합도를 가진 설계를 얻을 수 있음 

 

 

BDD 

Behavior Driven Development 

1. 시나리오 기반으로 테스트 코드를 작성하는 방법 

 


비밀번호 유효성 검증기 만들어보기 

 

요구사항 

- 비밀번호는 최소 8자 이상 12자 이하여야 한다.

- 비밀번호가 8자 미만 또는 12자 초과인 경우 IllegalArgumentException 예외를 발생시킨다.

- 경계조건에 대해 테스트 코드를 작성해야 한다. 

 

 


 

 

 

테스트 코드에서 

.doesNotThrowAnyException();    // 예외가 발생하지 않으면 => 통과

 

예외가 발생하지 않으면 통과하는 조건으로 코드를 작성한다. 

 

 

테스트코드가 통과하는 것을 확인하고 프로덕션 클래스를 리팩토링한다. 

 

 

TDD 흐름에 따라 

 

하드코딩으로 테스트가 일단 통과하는 것을 확인하면 리팩토링

 

 

최종적으로 리팩토링을 하고 다시 돌려봤을 때 

 

package org.example;

public class PasswordValidator {
    public static void validate(String password) {
        int length = password.length();
        if (length < 8 || length > 12){
            throw new IllegalArgumentException("비밀번호는 최소 8자 이상 12자 이하여야 한다.");
        }

    }
}

 

 

통과한다면 심리적 안정감을 얻으며 다음 진행을 할 수 있다. 

 

즉 다음과 같은 리팩토링을 추가로 진행해도 테스트 코드가 성공한 전제에 따라 쉬운 진행이 가능하다. 

 

package org.example;

public class PasswordValidator {

    public static final String WRONG_PASSWORD_LENGTH_EXCEPTION_MESSAGE = "비밀번호는 최소 8자 이상 12자 이하여야 한다.";

    public static void validate(String password) {
        int length = password.length();
        if (length < 8 || length > 12){
            throw new IllegalArgumentException(WRONG_PASSWORD_LENGTH_EXCEPTION_MESSAGE);
        }

    }
}

 


마찬가지로 다음 조건들에 대해서도 아래와 같은 테스트 코드 작성 

 

@DisplayName("비밀번호가 8자 미만 또는 12자 초과하는 경우 IllegalArgumentException 예외가 발생한다.")
@Test
void validatePasswordTest2(){

    assertThatCode(()-> PasswordValidator.validate("aabb")) // 위의 조건을 만족하면 조건을 만족하므로 예외가 발생 x
            .isInstanceOf(IllegalArgumentException.class)           // 통과하지 못했을 경우이므로 메시지를 출력하는데
            .hasMessage("비밀번호는 최소 8자 이상 12자 이하여야 한다.");   // 메인에서 설정한 메시지
}

 

첫번째 테스트의 경우 12자 미만이기 때문에 통과 성공 

두번째 테스트의 경우 8자 미만이기 때문에 통과 못함

각각에 맞는 예외처리 형식으로 테스트를 돌려준다. 

 

다음 3번째 조건인 경계조건에 대한 테스트 코드는 다음과 같이 작성한다. 

 

@DisplayName("비밀번호가 8자 미만 또는 12자 초과하는 경우 IllegalArgumentException 예외가 발생한다.")
@ParameterizedTest
@ValueSource(strings = {"aabbcce", "aabbccddeeffg"})
void validatePasswordTest2(){

    assertThatCode(()-> PasswordValidator.validate("aabb")) // 위의 조건을 만족하면 조건을 만족하므로 예외가 발생 x
            .isInstanceOf(IllegalArgumentException.class)           // 통과하지 못했을 경우이므로 메시지를 출력하는데
            .hasMessage("비밀번호는 최소 8자 이상 12자 이하여야 한다.");   // 메인에서 설정한 메시지
}

 

 

 


 

 

패스워드 형성을 위한 자바 라이브러리 추가 

implementation 'org.passay:passay:1.6.2'

 

랜덤으로 패스워드를 형성해주는 메소드 작성 

 

 

 

+ 유저가 만든다는 것을 클래스로 설정 

 

package org.example;

public class User {
    private String password;

    public void initPassword(){
        RandomPasswordGenerator randomPasswordGenerator = new RandomPasswordGenerator();
        String randomPassword = randomPasswordGenerator.generatePassword();

        /**
         * 비밀번호는 최소 8자 이상 12자 이하여야 한다.
         */

        if (randomPassword.length() >= 8 && randomPassword.length() <=12){
            this.password = randomPassword;
        }
    }
}

 

랜덤으로 만들어진 패스워드가 조건을 만족할 때 통과되는지를 테스트하면 된다. 

 

이와 같은 상황은 이번에는 Production 부분이 먼저 만들어지고 그 다음 테스트가 형성되는 과정이다. 

 

 

 

유저를 만들고 유저가 패스워드를 초기화하는 상황을 설정한다. 

 

void passwordTest() {
    User user = new User();
    user.initPassword();
}

 

메소드가 호출되었을 떼 가정하는 상황을 설정된 테스트 코드를 작성한다. 

 

class UserTest {
    @DisplayName("패스워드 초기화 여부를 판단한다.")
    @Test
    void passwordTest() {

        // 유저 객체가 주어지고
        User user = new User();

        // 메소드가 호출되었을 때
        user.initPassword();

        // 가정한다
        assertThat(user.getPassword()).isNotNull();

    }
}

 

이때 랜덤으로 만들어지는 패스워드는 이와 같은 코드 설정에 따라 조건에 맞을 때만 세팅이 된다. 

 

 

 

=> 성공 할 때가 있고 안할 때가 있게 된다 

 

 

여기서의 문제는 랜덤패스워드 제너레이터가 어떻게 패스워드를 만들지 컨트롤을 할 수 없게 된다는 데에 있다. 

 

 

이를 해결하는 방법으로서 인터페이를 선언해주고 랜덤패스워드 생성클래스가 이를 상속하게 하여 

실제 패스워드를 받아올 때 인터페이스로부터 받아오게하는 방식을 고려한다. 

 

먼저 테스트 코드에서 항상 통과하도록 하드코딩된 세팅을 테스트한다. 

 

class UserTest {
    @DisplayName("패스워드 초기화 여부를 판단한다.")
    @Test
    void passwordTest() {

        // 유저 객체가 주어지고
        User user = new User();

        // 메소드가 호출되었을 때
        user.initPassword(new CorrectFixedPasswordGenerator()); // 항상 8자인 정상인 패스워드 상태를 형성

        // 가정한다
        assertThat(user.getPassword()).isNotNull();
    }
}
package org.example;

public class CorrectFixedPasswordGenerator implements PasswordGenerator{
    @Override
    public String generatePassword(){
        return "abcdefgh";  // 8자리의 제대로된 패스워드를 받는 것으로 설정
    }

}

 

 

 

마찬가지로 항상 틀리게 되는 테스트 세팅을 설정한다. 

 

@DisplayName("패스워드가 요구사항에 부함하지 않아 초기화가 되지 않는 상황.")
@Test
void passwordTest2() {

    // 유저 객체가 주어지고
    User user = new User();

    // 메소드가 호출되었을 때
    user.initPassword(new WrongFixedPasswordGenerator()); // 항상 8자인 정상인 패스워드 상태를 형성

    // 가정한다
    assertThat(user.getPassword()).isNull();
}

 

 

package org.example;

public class WrongFixedPasswordGenerator implements PasswordGenerator{
    @Override
    public String generatePassword(){
        return "adf";  // 항상 틀린 것을 리턴
}
}

 

기존에는 내부에서 랜덤으로 패스워드가 형성되고 그것을 받고 있었는데 

 

조건에 맞으면 생성이 제대로 되지만 안 맞으면 틀리기 생성이 안되기 때문에 테스트가 불가했다. 

 

인터페이스를 구현해서 테스트 코드가 적용된 상황이 항상 통과하도록 설정

 

인터페이스로 구현된 유저 코드 부분 

package org.example;

public class User {
    private String password;

    public void initPassword(PasswordGenerator passwordGenerator){
        String Password = passwordGenerator.generatePassword();

        /**
         * 비밀번호는 최소 8자 이상 12자 이하여야 한다.
         */

        if (Password.length() >= 8 && Password.length() <=12){
            this.password = Password;
        }
    }

    public String getPassword() {
        return password;
    }
}

 

이것을 to-be 방식이라 할 수 있고 

 

더 낮은 결합도를 가진 설계를 얻을 수 있다. 

 

 

 

 

 

 

 

반응형