테스트 주도로 개발하기
TDD 시작하기
테스트 주도 개발!
요구사항, 결과 출력 화면만 보고 필요 객체, 설계를 예상하면서 만드는 궁예가 되지말자
기능 상 아주 작은 부분으로 쪼개서 실패 테스트 케이스를 작성하고 성공 케이스로 만들기위해 설계, 코드 리팩토링을 하자
한꺼번에 설계를 해두고 개발하는게 아니라 작은 기능부터 설계하고 개발하면서 설계를 리팩토링 하는 방식으로 개발하자는 것!
TDD로 개발해보기
- 코드스쿼드 프로그래밍 미션을 일부 가져와서 TDD 예시를 들어보려한다.
- 로또 프로그램 진행 화면 : 당첨번호 6개를 입력받아 로또 인스턴스를 생성해야하는 과정이다.
지난 주 당첨 번호를 입력해 주세요.
1, 2, 3, 4, 5, 6
-
기능 분석하기 사용자로부터 6개의 숫자를 입력 받는다.
-
테스트 코드를 짜두고 설계, 구현 완성하기 문자열로 입력 받는데, 숫자(현재 단계에선 최종 리턴 타입은 아직 정하지않음)로 만들려면 split 해야한다.
public class InputViewUtilsTest {
@Test
public void 문자열_하나씩_쪼개기() {
String numbers = "1, 2, 3, 4, 5, 6";
assertEqauls(new String[]{"1", "2", "3", "4", "5" ,"6"}, splitNumbers(numbers));
}
}
public class InputViewUtils {
public static String[] splitNumbers(String numbersMessage) {
return numbersMessage.split(",\\s*");
}
}
- 테스트 코드 구성 먼저 : ParserTest의 이름도 처음 테스트 코드를 짤 때는 중요하지않음
- 자연스럽게 설계가 된다 : 성공 케이스로 바꾸기 위해서 누군가(객체) 메소드를 맡아야하고, 메소드를 구현해야한다.
- split 결과를 숫자로 변환해야한다.
public class InputViewUtilsTest {
@Test
public void 문자열_하나씩_쪼개기() {
String numbers = "1, 2, 3, 4, 5, 6";
assertEqauls(new String[]{"1", "2", "3", "4", "5" ,"6"}, splitNumbers(numbers));
}
@Test
public void 문자열배열_숫자로_변환하기() {
String[] numbers = {"1", "2", "3", "4", "5" ,"6"};
assertThat(Arrays.asList(numbers), convertNumbersMessage(numbers));
}
}
public class InputViewUtils {
public static String[] splitNumbers(String numbersMessage) {
return numbersMessage.split(",\\s*");
}
public static int convertToNumber(String number) {
return Integer.parseInt(number);
}
public static List<Integer> convertNumbersMessage(String[] numbers) {
List<Integer> convertedNumbers = new ArrayList<>();
for (String number : numbers) {
convertedNumbers.add(convertToNumber(number));
}
}
}
-
먼저 실패 테스트 케이스를 작성해두고 리턴 타입을 정해야한다.
-
변환하려고 봤더니, 사용자 입력 중 숫자가 아닌 다른 문자가 입력되었을 수도 있다. 예외 처리를 구현해야해!
public class InputViewUtilsTest {
@Test
public void 문자열_하나씩_쪼개기() {
String numbers = "1, 2, 3, 4, 5, 6";
assertEqauls(new String[]{"1", "2", "3", "4", "5" ,"6"}, splitNumbers(numbers));
}
@Test
public void 문자열배열_숫자로_변환하기() {
String[] numbers = {"1", "2", "3", "4", "5" ,"6"};
assertThat(Arrays.asList(numbers), convertNumbersMessage(numbers));
}
// 예외가 예상되는 부분에 대해 테스트 케이스를 작성해두고 제대로 캐치하기위해 리팩토링을 한다.
@Test(expected = IllegalArgumentException.class)
public void 문자열배열_중_숫자아닌_요소포함() {
String[] numbers = {"1", "2", "3", "hello", "5" ,"6"};
convertNumbersMessage(numbers);
}
}
public class InputViewUtils {
public static String[] splitNumbers(String numbersMessage) {
return numbersMessage.split(",\\s*");
}
public static int convertToNumber(String number) {
try {
return Integer.parseInt(number);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("숫자만 입력해야합니다.");
}
}
public static List<Integer> convertNumbersMessage(String[] numbers) throws Illegal...{
}
}
- 유틸리티 메소드를 호출 하는 쪽에서 예외처리를 할 수 있도록 예외를 던져버린다.
- 중복된 입력 등등 위처럼 하나씩 잘게잘게 생각하면서 구현해나가는 것이 핵심이다.
- 번호를 쪼개었다고 가정하고, 로또로 만들어야한다.
- 로또 번호 길이가 6개인지 체크해야하는데, 유틸리티가 판단해야할까? 로또 번호 길이가 바뀐다면?
- 번호 범위는 1 ~ 45인데, 해당하지않는 숫자가 있을 수 있다. 유틸리티가 판단할 수 있을까? 로또 번호의 범위가 바뀐다면?
public class Input {
}
public class LottoTest {
@Test(expected = IllegalArgumentException.class)
public void 로또_구성숫자_길이_부족() {
new Lotto(Arrays.asList(7, 19, 3, 24, 38, 34, 5, 2));
}
@Test(expected = IllegalArgumentException.class)
public void 로또_숫자_범위아닌_숫자포함() {
new Lotto(Arrays.asList(0, -5, 46, 3, 6, 19));
}
}
public class Lotto {
private static final int MIN = 1;
private static final int MAX = 45;
private static final int NUM = 6;
private List<Integer> numbers;
public Lotto(List<Integer> numbers) {
if (isInvalidLengthNumbers(numbers)) {
throw new IllegalAragumentException(LOTTO_NUM + "개 숫자 입력해야함");
}
if (isIncludeOutRangeNumber(numbers)) {
throw new IllegalArgumentException("유효하지않은 범위의 숫자가 포함되어있음")
}
this.numbers = numbers;
}
private static boolean isInvalidLengthNumbers(List<Integer> numbers) {
return numbers.size() != NUM;
}
private static boolean isIncludeOutRangeNumber(List<Integer> numbers) {
return numbers.stream().anyMatch(number -> number < MIN || number > MAX);
}
}
- 한 단계씩 만들어나가면서 클래스 분리를 하기위해 던져야할 질문 : 얘가 해야할까? 변경 상황이 닥쳤을 때 어떻게되지?
- 앞서 언급했듯 궁예처럼 이런 클래스가 필요할거야! 하지않고, 그 역할이 필요한 상황에 추가해나간다.
이런 점이 좋더라
- 작게 작게 생각하게되고, 일단 기능이 되게끔 개발해두고 예외처리를 하지않고 필요한 예외처리를 하니 부담이 없어진다.
- 코딩하는게 게임처럼 되어버림, 테스트 케이스 성공시키기 게임
- 경험하기 전에는 설계를 막 해두고 개발하다가 이게 정말 아닌 것 같으면 git reset을 부르짖었지만…… 작게 작게 테스트하면서 개발하니깐 필요한 클래스 만들기, 메소드 분리가 자연스럽게 된다.
- 리팩토링을 의식적으로 이때해야지! 가 아니라 틈틈이 리팩토링을 하니 후처리 작업이라고 따로 부를게 없어진다.