모든 코드는 여기를 클릭 하시면 확인 하실 수 있습니다.
안녕하세요. 저번 포스팅에 이어 TDD로 개발하기 2탄입니다. 이번 포스팅에서는 TDD란 무엇인지에 대해 간략하게 알아보고 개발을 시작하려 합니다.
테스트 주도 개발(Test-driven development TDD)은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다. 개발자는 먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기 위한 최소한의 코드를 생성한다. 마지막으로 작성한 코드를 표준에 맞도록 리팩토링한다. 이 기법을 개발했거나 '재발견' 한 것으로 인정되는 Kent Beck은 2003년에 TDD가 단순한 설계를 장려하고 자신감을 불어넣어준다고 말하였다. - 위키피디아
위키피디아에서 정의한 TDD란 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나라고 되어 있는데요. 주관적으로 받아들인 의미로 간략하게 적어보자면 기존에는 Production Code 코드를 작성하고 이를 검증하는 테스트 코드를 작성하였다면 TDD 는 반대로 테스트를 먼저 작성하고 이를 통과시키게 하는 프로덕션 코드를 작성하는 것을 의미합니다. 보고자 하는 결과를 정의하고 그 결과를 통과시키는 최소한의 코드를 작성 하여 테스트를 통과시킨 후 리팩토링을 통해 프로그램 표준에 맞는 코드로 변경하는 사이클 자체를 TDD라고 정의하는 것 같습니다. TDD가 숙달되고 의미를 더 잘 이해하게 될 때 더 구체적으로 수정하겠습니다.
앞서 포스팅 한 TODO-List에 따라 구현 할 예정입니다. 기능 위주로 정리해서 올리고 마지막에 특별한 기술이나 특정 개념을 사용한 이유에 대해 작성하겠습니다.
package kail.study.java.racing;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class UserInputTest {
@Test
@DisplayName("올바르게 ,를 구분으로 입력하는 경우")
void splitTest() {
List<String> names = StringUtils.parseByComma("pobi,hodol,kyle");
assertThat(names).containsExactly("pobi","hodol","kyle");
assertThat(names).hasSize(3);
}
}
package kail.study.java.racing;
import java.util.Arrays;
import java.util.List;
public class StringUtils {
public static List<String> parseByComma(String userInput) {
return Arrays.asList(userInput.split(","));
}
}
package kail.study.java.racing;
import java.util.Scanner;
public class InputView {
private static final Scanner sc = new Scanner(System.in);
public static String getInput() {
return sc.nextLine();
}
}
package kail.study.java.racing;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class NameTest {
@ParameterizedTest
@ValueSource(strings = {"kyle","hodle","pobi"})
@DisplayName("이름 규칙에 문제가 없어서 정상적으로 생성되는 경우")
void 정상적인_경우(String name) {
assertThat(new Name(name));
}
}
package kail.study.java.racing;
public class Name {
private final String name;
public Name(String name) {
this.name = name;
}
}
@ParameterizedTest
@ValueSource(strings = {"overLength", "helloWorld", "ImTheKing"})
void 길이_초과(String name) {
assertThatThrownBy(() -> {
new Name(name);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("길이 초과");
}
public class Name {
private final String name;
public Name(String name) {
validateOverLength(name);
this.name = name;
}
private void validateOverLength(String name) {
if (name.length() > 5 || name.length() < 1)
throw new IllegalArgumentException("길이가 초과되었습니다.");
}
}
public class Name {
public static final int MAX_LENGTH = 5;
private final String name;
public Name(String name) {
validate(name);
this.name = name;
}
private void validate(String name) {
checkLength(name);
}
private void checkLength(String name) {
if (name.length() > MAX_LENGTH)
throw new IllegalArgumentException("길이가 초과되었습니다.");
}
}
@ParameterizedTest
@ValueSource(strings = {"", " ", " "})
void 공백_예외(String name) {
assertThatThrownBy(() -> {
new Name(name);
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("공백은 입력할 수 없습니다.");
}
@Test
void 널이라서_예외() {
assertThatThrownBy(() -> {
new Name(null);
}).isInstanceOf(NullPointerException.class)
.hasMessageContaining("널값은 입력할 수 없습니다.");
}
public Name(String name) {
validate(name);
this.name = name;
}
private void validate(String name) {
checkNull(name);
checkBlank(name);
checkLength(name);
}
private void checkNull(String name) {
if(name == null)
throw new NullPointerException("널값은 입력할 수 없습니다.");
}
private void checkBlank(String name) {
if(name.trim().isEmpty())
throw new IllegalArgumentException("공백은 입력할 수 없습니다.");
}
@Test
void 중복된_이름 () {
Name pobi = new Name("pobi");
assertThatThrownBy(()->{
new Name("pobi");
}).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("중복된 이름이 존재합니다.");
}
public class Names {
private final List<Name> names;
public Names(List<Name> names) {
validate(names);
this.names = names;
}
private void validate(List<Name> names) {
checkDuplication(names);
}
private void checkDuplication(List<Name> names) {
Set<Name> set = names.stream().collect(Collectors.toSet());
if(set.size() != names.size())
throw new IllegalArgumentException("중복된 이름이 존재합니다.");
}
}
String name = "kyle"
의 형태로 작성할 수도 있지만 이런 경우 의미 없는 굉장히 추상화된 값이 됩니다. 추상화가 심해진다는 것은 프로그래머가 타 프로그래머에게 본인의 코드를 설명하기 위한 설명이 길어질 뿐 아니라 프로그램이 커질 수록 이는 거의 불가능에 가까워 집니다. 하지만 이를 클래스의 형태로 한 번 포장하게 되면 아주 사소한 값이라도 컴파일러 뿐 아니라 프로그래머에게 왜 그리고 어떤 값을 의미하는 지를 명확하게 전달 할 수 있게 됩니다.