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

1.문서화역할
2.코드에 결함을 발견하기 위함
3.리팩토링 시 안정성 확보
4.테스트 하기 쉬운 코드를 작성하다 보면 더 낮은 결합도를 가진 설계를 얻을 수 있음.

TDD


• Test Driven Development (테스트 주도 개발)
• 프로덕션 코드보다 테스트 코드를 먼저 작성하는 개발 방법
• 기능 동작을 검증 (메서드 단위)

BDD

• Behavior Driven Development (행위 주도 개발)
• 행위에 대한 테스트 코드를 작성하는 개발 방법
• 유저 시나리오 동작을 검증 (시나리오 단위)
• 하나의 시나리오는 Given, When, Then 구조를 가짐

테스트 코드를 작성하는 이유와 작성방법의 유형을 배우고 실습을 진행해보았다. 다음과 같은 요구조건을 따르는 테스코드를 작성해보았다.

요구사항
• 비밀번호는 최소 8자 이상 12자 이하여야 한다.
• 비밀번호가 8자 미만 또는 12자 초과인 경우 IllegalArgumentException 예외를 발생 시킨다.
• 경계조건에 대해 테스트 코드를 작성해야 한다.

build.gradle

    implementation 'org.passay:passay:1.6.1'
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2'
    testImplementation("org.assertj:assertj-core:3.24.2")

처음 프로덕션 코드보다도 테스트 코드를 먼저 작성한다 (TDD)

 	@DisplayName("비밀번호가 최소 8자 이상, 12자 이하면 에외가 발생하지 않는다.")
    @Test
    void validatePasswordTest() {
        assertThatCode(() -> PasswordValidator.validate("serverwizard"))
                .doesNotThrowAnyException();;
    }

이대로 실행하면 안돌아감. PasswordValidator를 만들지 않았으니까
-> 실패하는 테스트 작성

Passwordvalidator

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

PasswordValidator 클래스를 만들어주고 다시 테스트를 실행하면 제대로 작동한다.(servierwizard는 12자의 비밀번호이므로)
-> 테스트를 통과하는 작은 코드 작성

그런데 프로덕션 코드에서 password.length()는 지역변수로 바꿀 수 있을것 같다. 예외 메시지는 전역변수로 처리 할 수 있을 것 같으니

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차 초과하는 경우 IllegalArgumentExceptions 예외가 발생한다.")
    @ParameterizedTest
    @ValueSource(strings = {"aabbcce", "aabbccddeeffg"})
    void validatePasswordTest2(String password) {
        assertThatCode(() -> PasswordValidator.validate(password))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("비밀번호는 최소 8자 이상 12자 이하여야한다.");
    }

이제 여러 값에 대해 테스트를 진행 해볼텐데 여러가지 변수에대해 번거롭게 테스트하기는 귀찮고 힘이 드니 JUnit에서 제공되는 어노테이션을 통해 테스트를 해보자

@ParameterizedTest

Junit에서는 @ParameterizedTest 라는 어노테이션을 제공한다. 기본적인 사용 방법은 @Test 대신 @ParameterizedTest라는 어노테이션을 사용하는 것 외에는 동일하다.

이 때 파라미터로 넘겨줄 값들을 지정해주어야 하는데, 이 역시 어노테이션을 사용해서 테스트에 주입해줄 수 있다.

@ValueSource

테스트에 주입할 값을 어노테이션에 배열로 지정한다.
테스트를 실행하면 배열을 순회하면서, 테스트 메소드에 인수로 배열에 지정된 값들을 주입해서 테스트한다. 이 때, 하나의 테스트에는 하나의 인수(argumnet)만 전달할 수 있다.
@ValueSource에 사용할 수 있는 자료형은 다음과 같다.

  • byte, short, int, long, float, double, char, boolean
  • String, Class

Test2

	@DisplayName("비밀번호가 8자 미만 또는 12차 초과하는 경우 IllegalArgumentExceptions 예외가 발생한다.")
    @ParameterizedTest
    @ValueSource(strings = {"aabbcce", "aabbccddeeffg"})
    void validatePasswordTest2(String password) {
        assertThatCode(() -> PasswordValidator.validate(password))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("비밀번호는 최소 8자 이상 12자 이하여야한다.");
    }


위와 같이 실행된다.

이제 랜덤 비밀번호를 만드는 객체와 User객체를 만들어서 테스트를 진행해보겠다.

RandomPasswordGenerator

public class RandomPasswordGenerator {
    public static final String ALLOWED_SPL_CHARACTERS = "!@#$%^&*()_+";

    public static final String ERROR_CODE = "ERRONEOUS_SPECIAL_CHARS";


    public String generatePassword() {
        PasswordGenerator gen = new PasswordGenerator();

        CharacterData lowerCaseChars = EnglishCharacterData.LowerCase;
        CharacterRule lowerCaseRule = new CharacterRule(lowerCaseChars);
        lowerCaseRule.setNumberOfCharacters(2);

        CharacterData upperCaseChars = EnglishCharacterData.UpperCase;
        CharacterRule upperCaseRule = new CharacterRule(upperCaseChars);
        upperCaseRule.setNumberOfCharacters(2);

        CharacterData digitChars = EnglishCharacterData.Digit;
        CharacterRule digitRule = new CharacterRule(digitChars);
        digitRule.setNumberOfCharacters(2);

        CharacterData specialChars = new CharacterData() {
            public String getErrorCode() {
                return ERROR_CODE;
            }

            public String getCharacters() {
                return ALLOWED_SPL_CHARACTERS;
            }
        };
        CharacterRule splCharRule = new CharacterRule(specialChars);
        splCharRule.setNumberOfCharacters(2);

        // 0 ~ 12
        return gen.generatePassword((int) (Math.random() * 13), splCharRule, lowerCaseRule, upperCaseRule, digitRule);
    }
}

User

public class User {
    private String password;

    public String getPassword() {
        return password;
    }

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

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

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


    }
}

랜덤한 비밀번호를 만들어주는 RandomPasswordGenerator를 주입받아 User의 initPassword 메서드에서 랜덤비밀번호가 조건에 맞으면 User의 비밀번호를 초기화해주는 것이다..

class UserTest {

    @DisplayName("패스워드를 초기화한다")
    @Test
    void passwordTest() {
        //given
        User user = new User();

        //when
        user.initPassword();

        //then
        assertThat(user.getPassword()).isNotNull();
    }
}

이 테스트 코드를 실행하게 되면 성공할때도 실패할때도 있다. 이유는 뭘까?

return gen.generatePassword(
(int) (Math.random() * 13), splCharRule, lowerCaseRule,
upperCaseRule, digitRule);

에서 0 ~ 12자리의 비밀번호를 제공해주는데 initPassword에서는 8자이상 12자이하 비밀번호만 초기화가 되기때문에 8자미만의 비밀번호가 생성되면 비밀번호가 초기화 되지않아 Null 값이 나오기 때문이다.
이 상황은 내가 비밀번호의 길이를 조절 할 수 없기 때문이다. 그럼 쉬운 테스트 코드를 만들려면 어떻게 해야할까

우선 하나의 interface를 생성해준다

public interface PasswordGenerator {
    String generatePassword();
}

이제 테스트폴더에 interface를 구현하는 두 개의 클래스를 만들겠다..

CorrectFixedPasswordGenerator

public class CorrectFixedpasswordGenerator implements  PasswordGenerator{

    @Override
    public String generatePassword() {
        return "abcdefgh"; //8글자
    }
}

WrongFixedPasswordGenerator

public class WrongFixedpasswordGenerator implements  PasswordGenerator{

    @Override
    public String generatePassword() {
        return "ab"; //2글자
    }
}

그렇담 User객체의 initPassword메서든 다음과같이 수정한다

    public void initPassword(PasswordGenerator passwordGenerator) {
        String password = passwordGenerator.generatePassword();
        /*
        * 비밀번호는 최소 8자 이상 12자 이하여야 한다.
        * */
        if(password.length() >= 8 && password.length() <= 12) {
            this.password = password;
        }
    }

**PasswordGenerator에서도 interface를 implements 해준다!

interface를 구현한 클래스들의 테스트 코드는 다음과같다.

class UserTest {

    @DisplayName("패스워드를 초기화한다")
    @Test
    void passwordTest() {
        //given
        User user = new User();

        //when
        user.initPassword(new CorrectFixedpasswordGenerator());

        //then
        assertThat(user.getPassword()).isNotNull();
    }

    @DisplayName("패스워드가 요구사항에 부합되지 않아 초기화가 되지 않는다.")
    @Test
    void passwordTest2() {
        //given
        User user = new User();

        //when
        user.initPassword(new WrongFixedpasswordGenerator());

        //then
        assertThat(user.getPassword()).isNull();
    }
}

'new CorrectFixedpasswordGenerator()'는 항상 8자의 비밀번호를 생성하니 비밀번호가 초기화되어 Null이 나오지 않고,
'new WrongFixedpasswordGenerator()'는 항상 2자의 비밀번호가 생성되어 비밀번호가 초기화 되지 않으니 Null이 나오게 된다.
왜 이렇게 하는 것일까?

RandomPasswordGenerator randomPasswordGenerator =
	new RandomPasswordGenerator();

initPassword 메서드에서 RandomPasswordGenerator를 의존받아 사용하다보니 강한 결합이 발생하여 테스트코드를 쉽게 작성하기 어려운 구조였기 때문이다. 이로 인해 테스트 하기 쉬운 코드를 작성하다 보면 더 낮은 결합도를 가진 설계를 얻을 수 있음을 알게 되었다.

번외

@FunctionalInterface
public interface PasswordGenerator {
    String generatePassword();
}

위와 같이 메서드가 하나인 interface는 @FunctionalInterface 어노테이션(Java8부터)을 붙여서 함수형 인터페이스로 사용 할 수 있다.

    @DisplayName("패스워드를 초기화한다")
    @Test
    void passwordTest() {
        //given
        User user = new User();

        //when
        user.initPassword(() -> "abcdefgh"); //람다식 사용가능

        //then
        assertThat(user.getPassword()).isNotNull();
    }

그럼 위와 같이 람다식으로 사용 할 수도 있다.

profile
개발자 되고싶다..

0개의 댓글