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

• Test Driven Development (테스트 주도 개발)
• 프로덕션 코드보다 테스트 코드를 먼저 작성하는 개발 방법
• 기능 동작을 검증 (메서드 단위)
• 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에서 제공되는 어노테이션을 통해 테스트를 해보자
Junit에서는 @ParameterizedTest 라는 어노테이션을 제공한다. 기본적인 사용 방법은 @Test 대신 @ParameterizedTest라는 어노테이션을 사용하는 것 외에는 동일하다.
이 때 파라미터로 넘겨줄 값들을 지정해주어야 하는데, 이 역시 어노테이션을 사용해서 테스트에 주입해줄 수 있다.
테스트에 주입할 값을 어노테이션에 배열로 지정한다.
테스트를 실행하면 배열을 순회하면서, 테스트 메소드에 인수로 배열에 지정된 값들을 주입해서 테스트한다. 이 때, 하나의 테스트에는 하나의 인수(argumnet)만 전달할 수 있다.
@ValueSource에 사용할 수 있는 자료형은 다음과 같다.
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객체를 만들어서 테스트를 진행해보겠다.
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);
}
}
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를 구현하는 두 개의 클래스를 만들겠다..
public class CorrectFixedpasswordGenerator implements PasswordGenerator{
@Override
public String generatePassword() {
return "abcdefgh"; //8글자
}
}
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();
}
그럼 위와 같이 람다식으로 사용 할 수도 있다.