📜 제목으로 보기

TDD

red -> green -> refactoring

  1. 실패하는 코드(red)부터 짠다.

    1. 테스트코드부터 짠다.
    2. 프로덕션 코드를 생성하지 않은 체로 작성하면서 생성한다.
    3. given(data), when(actual), then(assert문)의 순서로 짠다.
    4. 최초 에러를 내기 위해 throw new IllegalStateException();를 활용한다.
  2. 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번째부터는 일반화된 풀 구현
        }
      
  3. 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();
        }
              
      
    • 학습테스트 통과한 코드를 —> 프로덕션 코드에 반영하자

일부분 수정(private함수or 코드) TDD -> 영향받지 않게 복붙메서드 활용

[private ] 특정함수 수정 TDD -> Test에 [특정함수]만 복붙
  • 요구사항: 음수 -> 예외발생시키세요. -> split한 리팩토링으로 splitAndSum 중 private sum()함수만 TDD해야한다.

    1. Test클래스로 private함수를 복붙해온다.

      • 복붙을 한 순간, 기존 코드에 영향주지 않는 코드가 된다.
      • XXX.sum() -> XXXTest.sum()으로 사용해서 테스트할 예정이다.
    2. 전체 메서드(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;
          }
        
    3. 예외처리 로직을 중간에 추가 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;
        
    4. 테스트통과시 복붙한 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);
        }
      
    • 테스트가 완료되면 수정된 일부 코드만 기존함수에 반영하고, 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() )
  • assert문을 여러개하지말고, 테스트를 추가하다보면, 일반화할 수 밖에 없다.

강타입 vs 약타입

  • 타입 : 자료형이 맞지 않을 시에 에러 발생, 암묵적 변환을 지원하지 않음
  • 타입 : 자료형이 맞지 않을 시에 암묵적으로 타입을 변환하는 언어

정적타입 vs 동적타입

  • 정적타입 언어 : 정적타입 언어는 컴파일 시에 변수의 타입이 결정되는 언어를 의미한다.
  • 동적타입 언어 : 동적타입 언어는 런타임 시 자료형이 결정되는 언어를 의미한다.