TDD 1 문자열계산기
네오의 TDD 강의(문자열계산기)+TIP
📜 제목으로 보기
TDD
red -> green -> refactoring
-
실패하는 코드(
red
)부터 짠다.- 테스트코드부터 짠다.
- 프로덕션 코드를 생성하지 않은 체로 작성하면서 생성한다.
- given(data), when(actual), then(assert문)의 순서로 짠다.
-
최초 에러를 내기 위해
throw new IllegalStateException();
를 활용한다.
-
green
을 만들 때는, 모든case 통과 풀 구현이 아니라, (현재까지의 테스트를 모두 만족시키는) 최소한의 최소한의 수정만 한다.-
예를 들어
expected 1
을 만족시키는 코드를 짤 때-
return Integer.parseInt("1");
이 아닌return 1;
으로 짜야한다.
-
-
임의의 1개 case가 아닌
다양한 expected
를 만족시켜야할 경우, assert문을 여러개 짜지말고, @Test메서드를 늘려서 2번째부터는 일반화시키는 풀구현을 하자.@Test void splitAndSum_숫자하나() { final int actual = StringCalculator.splitAndSum("1"); assertThat(actual).isEqualTo(1); //assertThat(actual).isEqualTo(2); } // assert문 추가가 아니라 테스트를 추가한다. @Test void splitAndSum_숫자하나_2() { final int actual = StringCalculator.splitAndSum("2"); assertThat(actual).isEqualTo(2); //에러를 고치려면 어쩔 수 없이 2번째부터는 일반화된 풀 구현 }
-
-
refactoring
은-
메서드 추출 시, 새롭게 추출된 메서드의 관점에서 인자를 수정할 수 도 있다.
-
대상:
indent를 늘리는 반복문을 포함한 부분
or다른 일을 하는 곳
-
인자에
final
을 달아서 엄청 긴 코드의 끝에서도 안심하고 사용할 수 있게 해보자. 요즘 언어들은 불변을 default로 한다고 한다. -
매개변수의 이름도 바뀌어야한다
splitValues -> values
//private static int sum(String[] splitValues) { private static int sum(String[] values) { int result = 0; for (String value : values) { result += Integer.parseInt(value); } return result;
-
-
복잡한 요구사항 -> 쪼갠 학습테스트 활용
-
예를 들어, 문자열을 커스텀 문자열로 split -> sum 을 구해야하는데, 일단 split부터 되도록 학습테스트를 작성한다.
-
요구사항
@Test void splitAndSum_custom_구분자() { final int actual = StringCalculator.splitAndSum("//;\n1;2;3"); assertThat(actual).isEqualTo(6); }
-
단계별 학습테스트 추가 1
- 쪼갰을 때 나와야하는 값을 expected로 생각하고 테스트하면 된다.
@Test void split() { // given final String text = "//;\n1;2;3"; // split해보고 -> **내 기준에서 나와야하는 값(**이게 나와야한다**)을 <expected>자리에 넣고 테스트해본다.** // when String[] splitValues = text.split("\n"); assertThat(splitValues[0]).isEqualTo("//;"); // 앞부분에서 나와야하는 값이 통과했다면, -> 뒷부분도 테스트 코드를 추가해서 돌려본다. assertThat(splitValues[1]).isEqualTo("1;2;3"); }
-
단계별 학습테스트 추가2
- 잘 쪼개졌어도, 구분자가 기존 코드와는 다른 경우이므로, if 검사 후 -> ealry return 로직을 추가해야하는 상황이다.
- 특정 경우를 잘 골라내는지 boolean이 expected로 되는 학습테스트를 추가하자.
@Test void startsWith() { //given // data -> "//"로 시작하는지를 확인하는 학습테스트를 만든다. final String text = "//;\n1;2;3"; //when // actual -> 슬래쉬 2개로 시작하는 커스텀이냐? boolean isCustom = text.startsWith("//"); //then // expected -> 나와야하는 값 = true assertThat(isCustom).isTrue(); }
- 잘 쪼개졌어도, 구분자가 기존 코드와는 다른 경우이므로, if 검사 후 -> ealry return 로직을 추가해야하는 상황이다.
-
학습테스트 통과한 코드를 —> 프로덕션 코드에 반영하자
-
일부분 수정(private함수or 코드) TDD -> 영향받지 않게 복붙메서드 활용
[private ] 특정함수 수정 TDD -> Test에 [특정함수]만 복붙
-
요구사항: 음수 -> 예외발생시키세요. -> split한 리팩토링으로 splitAndSum 중
private sum()
함수만 TDD해야한다.-
Test클래스로 private함수를 복붙해온다.
- 복붙을 한 순간, 기존 코드에 영향주지 않는 코드가 된다.
- XXX.sum() ->
XXXTest
.sum()으로 사용해서 테스트할 예정이다.
-
전체 메서드(splitAndSum)중 일부분(sum())만 가져왔으므로
테스트 호출 메소드도 달라질 것이다.
-
기존 전체메서드 테스트 가정 (나중에 완성될 테스트)
@Test void splitAndSum_negative() { assertThatThrownBy(() -> StringCalculator.splitAndSum("-1,2,3")) .isInstanceOf(RuntimeException.class); }
-
일부분(전체에 비해
인자 달라짐
) 메서드 테스트 ( Test로 복사해온 private함수라XXXXTest
.sum() )void sum_negative() { // sum("-1,2,3"); // sum("-1,2,3".split()) // StringCalculatorTest.sum("-1,2,3".split(",")); assertThatThrownBy(() -> StringCalculatorTest.sum("-1,2,3".split(","))) .isInstanceOf(RuntimeException.class); }
-
Test코드로 복붙해온 일부분 메서드(
이놈을 수정해서 구현
후 반영할 예정 )private static int sum(String[] values) throws RuntimeException { int result = 0; for (String value : values) { result += Integer.parseInt(value); } return result; }
-
-
예외처리 로직을 중간에 추가 by 변수 추출
-
좌항 = 우항 으로
우항이 할당되기 전 <if 예외처리>로직 추가
를 위해서변수 추출(Ctrl+Alt+V)
을 활용한다.result += Integer.parseInt(value); //우항이 넘어가기 전에 받아준다. int number = Integer.parseInt(value); result += number; int number = Integer.parseInt(value); // 우항이 넘어가기전에 받아 예외처리를 해준다. if (number < 0) { throw new RuntimeException(); } result += number;
-
-
테스트통과시 복붙한 sum()메서드를 -> 원본에 반영 + 원본 테스트함수 돌려보기
- 끝난 코드는 날리는게 맞다.
-
[public ] 특정[코드] TDD -> 바로 밑에 [코드포함 222메서드 함수] 복붙
-
예를 들어, 어느정도 완성된
public splitAndSum()
메서드에서 text==null이외에 text.isEmpty() 검사를 추가하고 싶다. 하지만,이미 다른 코드들과 섞여있는 일부분
의 코드이므로다른 코드에 영향주지 않게 테스트
하려고 한다.-
코드를 포함한 메서드 복사 붙혀넣기
를 통해기존 코드 영향 받지 않는 메소드2를 새롭게 생성
해서 테스트한다.// 기존 메소드 public static int splitAndSum(final String text) { if (text == null) { return 0; } return sum(split(text)); } // if 조건에 .isEmpty()를 추가할 예정인데 -> sum(split(text)))가 섞여있으므로 // 코드 수정시 영향이 갈 수 있다. // -> 포함 메서드를 통째로 복붙한 메서드2로 테스트한다. // 대신 테스트할 메서드2 public static int splitAndSum2(final String text) { if (text == null || text.isEmpty()) { return 0; } return sum(split(text)); }
-
메서드2로 테스트하는 테스트메서드를 생성한다.
- 기존 null테스트에서
인자
+expected(나와야할 값)
+복붙한 메서드2()
로 이름 바꾸기
//splitAndSum2 를 테스트 @Test void splitAndSum_emtpy() { final int actual = StringCalculator.splitAndSum2(""); assertThat(actual).isEqualTo(0); }
- 기존 null테스트에서
-
테스트가 완료되면
수정된 일부 코드만 기존함수에 반영
하고, 222메서드랑 테스트는 날린다.if (text == null || text.isEmpty())
-
그외
- assertThat()을 여러게 사용하지마라 -> 테스트를 그만큼 만들다보면 일반화
- 혹온 @ParameterizedTest활용
- 테스트메서드는 영어로 대충 짓고 -> @DisplayName()으로 한글을 제대로 적어주자.
- private함수를 Test에 복붙해서 테스트하는 것외에
접근제어자를 지워 default
로 만들어서 테스트할 수 도 있다. - 메서드명은 snake_Case를 써도 된다.
상수화 네이밍
-
검증시
if ~ thr
로직 ->checkXXXX
-
index들 ->
CASE_CASE2_INDEX
/ 구분자 ->CASE_CASE2_DELIMITER
if (text.startsWith("//")){ String[] splitValues = text.split("\n"); String delimiter = splitValues[0].substring(2); String customText = splitValues[1]; return customText.split(delimiter); } if (text.startsWith(CUSTOM_PREFIX)){ String[] splitValues = text.split(CUSTOM_DELIMITER); String delimiter = splitValues[CUSTOM_DELIMITER_INDEX].substring(CUSTOM_PREFIX.length()); String customText = splitValues[CUSTOM_TEXT_INDEX]; return customText.split(delimiter); }
-
특정문자열
다음에 나오는 문자열
추출 -> substring(특정문자열.length()
)- 예를 들어, “//;”에서 ;를 추출하고 싶다면?
- ”//;”.substring( “//”.length())
- text.substring(
CUSTOM_DELIMITER_PREFIX
.length() )
- text.substring(
-
assert문을 여러개하지말고, 테스트를 추가하다보면, 일반화할 수 밖에 없다.
강타입 vs 약타입
-
강
타입 : 자료형이 맞지 않을 시에에러
발생, 암묵적 변환을 지원하지 않음 -
약
타입 : 자료형이 맞지 않을 시에암묵적으로 타입을 변환
하는 언어
정적타입 vs 동적타입
-
정적
타입 언어 : 정적타입 언어는컴파일 시에 변수의 타입이 결정
되는 언어를 의미한다. -
동적
타입 언어 : 동적타입 언어는런타임 시 자료형이 결정
되는 언어를 의미한다.