📜 제목으로 보기

  • 실패하는 코드를 짜는 이유?
    • 통과하는 코드를 짜려고 하면, case단위가 아니라 다 짜야해서 너무 커진다.
    • case 만드는 것이 목적 -> 실패 case부터 만든다.
  • 테스트코드도 복잡도를 낮춘다.

기능 요구사항 복붙후 정리

복붙

각 자동차에 이름을 부여할 수 있다. 
전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. 
자동차 이름은 쉼표(,)를 기준으로 구분한다. 
자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다.

제목(##) + 앞에 체크박스 만들기

## 요구사항
 
- [ ] 각 자동차에 이름을 부여할 수 있다.
- [ ] 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- [ ] 자동차 이름은 쉼표(,)를 기준으로 구분한다.
- [ ] 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다.

  • 각 요구사항들을 테스트코드 DisplayName으로 작성하기도 한다.

키워드 뽑기

## 요구사항
 
- [ ]  자동차에 이름을 부여할  있다.
- [ ] 전진하는 자동차를 출력할  자동차 이름을 같이 출력한다.
- [ ] 자동차 이름은 쉼표(,) 기준으로 구분한다.
- [ ] 자동차 경주 게임을 완료한  누가 우승했는지를 알려준다. 우승자는 한명 이상일  있다.
    
    
- 자동차
- 이름
- 자동차 이름은 쉼표(,) 기준
- 전진
- 게임
- 우승
- 우승자는 한명 이상

키워드 관계 재배치

  • 객체인지 vs 메서드인지 vs 세부 요구사항인지
  • 객체 중에 이름만 다르게 뽑을지 -> 뽑는 것은 관리자 객체에서
    • 우승한 자동차 -> 자동차 중 뽑은 것 -> 자동차 경주(관리자)
- 자동차
    - 전진: 자동차가 하는 것이니 객체가 아닌 메서드
    - 우승한 자동차: 따른 객체인지 vs 자동차 중 뽑아서 이름만 다르게?
        - 우승자는 한명 이상: 세부 요구사항
- 이름
    - 이름은 쉼표(,)를 기준: 세부 요구사항
- 자동차 게임: 자동차에서 우승한 자동차 뽑는 것은 이상하니, 게임이 관리하면서 뽑도록
    - 우승한 자동차: 따른 객체인지 vs 자동차 중 뽑아서 이름만 다르게?
        - 우승자는 한명 이상: 세부 요구사항

시작점 고르기

  • 자동차 게임 -> 관리자부터 하기엔 잘 모름
  • 자동차 -> 핵심이자 간단한 단위 객체
    • 진짜 잘 모르겠으면 이름부터 해도 된다.

출력, View에 대한 요구사항은 무시하고 객체 설계부터 시작

  • UI, view를 맨나중에 붙이면, 객체다운 객체가 완성됨.

TDD 시작

정의 없는 default생성자 사용 / 응답 하는 메서드부터

  • TDD 시작점을 new Car();의 생성자에서 부터 시작하는 경우가 많다.

    • 보통은 Car car = new Car();의 생성자로 TDD를 시작한다.
      • 여기서 시작해서 테스트 코드로 변하게 된다.
      @Test
      void 생성자_검사() {
          //방법1. 생성시 예외발생안함.
          assertDoesNotThrow(() -> new Car(재성));
        
          //방법2. 생성한 것이 null이 아님.
          final Car car = new Car("재성");
        
          assertThat(car).isNotNull();
      }
    
  • 여기서는 객체 생성자와 동시에 상수 먼저 응답하는 기능 메서드를 주 테스트로 시작한다

    • 움직였을 때, 1칸 움직였다고 -> 상수 1을 응답할거라 가정하고 작성한다.
      //1. 객체+Test로 테스트 class 작성
      public class CarTest {
          @Test
          //2. (생성자 따로 검사하는게 아닌) 객체의 기능으로 메서드 명 먼저 작성
          void move() {
              //3. 없어도 객체 생성자 호출
              final Car car = new Car();
        
              //4. 객체 기능을 호출하되, 테스트에 용이하도록 <호출시 응답 결과값>이 있을 거라 예상 -> assertThat( actual )에 바로 값 넣어주기
              //car.move()
        
              //5. 기대하는(나와야하는) 응답 값을 .isEqualTo(  expected )에 넣어주기
              assertThat(car.move()).isEqualTo(1);
          }
      }
    
  • 컴파일 에러들을 잡는다.

    • 예상하는 응답값을 상수로 바로 나오도록 먼저 작성한다.
      public class Car {
          public int move() {
              return 1;
          }
      }
    
  • 테스트를 돌려서, 상수 1로 응답하는지 확인한다.

case 추가(누적 -> 응답 expected 변경 2)

복붙으로 다른 Test case(메서드 맨끝_네임 수정)를 작성하며 안돌아가는 코드 구현

  • 1칸 움직여서 1 반환하는 case도 있지만, 2번 움직이는 or 안 움직이는 케이스도 있음.

    • 메서드를 복붙해서 case를 나눈다
      @Test
      void move_go() {
          final Car car = new Car();
        
          assertThat(car.move()).isEqualTo(1);
      }
        
      @Test
      void move_go2() {
          final Car car = new Car();
          //1.
          car.move();
          //2. 
          assertThat(car.move()).isEqualTo(2); // 2를 응답해줘야함.
      }
    

기존 작성된 메서드를 메서드2로 복붙한 뒤 구현

  • 기존 코드는 돌아가도록 하는 상태(move() 그대로 두기)에서 move() -> move2()로 복붙한 뒤, 직전꺼도 포함해서 다 돌아가도록 구현

      package domain;
        
      public class Car {
          public int move() {
              return 1;
          }
        
          public int move2() {
              return 1;
          }
      }
    
    • 누적되려면, 저장할 변수가 필요해서 필드(인변)를 선언하여 거기다가 가지고 있도록 한다..
      public int move2() {
          return position++;
      }
    
      public class Car {
          private int position;
        
          public int move() {
              return 1;
          }
        
          public int move2() {
              return ++position;
          }
      }
        
    

복붙 수정 메서드2를 사용해서 통과시켜본다.

  • 복붙new Test method 에서 복붙 method2()를 사용해서 통과시킨다.

      @Test
      void move_go2() {
          final Car car = new Car();
          //1.
          car.move2();
          //2.
          assertThat(car.move2()).isEqualTo(2);
      }
        
    

기존case도 수정 메서드2로 변경해보고 -> 기존메서드()는 회색띄운 뒤 삭제

  • 처음 테스트도 .move2()를 사용하게 한 뒤 테스트

  • 테스트 통과하면 기존 move()는 사용안되서 회색 떠있음 -> alt + del로 안전삭제

      @Test
      void move_go() {
          final Car car = new Car();
        
          assertThat(car.move2()).isEqualTo(1);
      }
        
      @Test
      void move_go2() {
          final Car car = new Car();
          //1.
          car.move2();
          //2.
          assertThat(car.move2()).isEqualTo(2);
      }
    

최종 수정메서드2 -> 매서드로 이름 변경

  • .move2()가 -> .move()를 대체하는 순간

      @Test
      void move_go() {
          final Car car = new Car();
        
          assertThat(car.move()).isEqualTo(2);
      }
        
      @Test
      void move_go2() {
          final Car car = new Car();
          //1.
          car.move();
          //2.
          assertThat(car.move()).isEqualTo(2);
      }
      }
    

case 추가(조건 따라 stop -> expected는 stop일때 나와야하는 값으로 미리 넣어두기 0)

복붙 Test case 추가 ( 설계전 해당 case expected입력해서 error)

  • move_stop케이스 추가 -> expected 0을 응답하도록 작성하기

      @Test
      void move_stop() {
          final Car car = new Car();
        
          //1. 1번움직였는데  --->  0을 응답해야하는 경우도 처리
          assertThat(car.move()).isEqualTo(0);
      }
    

메서드2()로 복사해서 다시 처리

  • 해당 조건일 때만 움직이게 해준다.

      public int move2() {
          if (ThreadLocalRandom.current().nextInt(10) >= 4) {
              position += 1;
          }
          return position;
      }
      }
    

내부 랜덤 포함 메서드는 테스트 실행시마다 결과가 다르다.

  • 특정 테스트 바깥에 커서를 두고 전체 테스트 실행
    • 랜덤이 껴있는 경우 돌릴 때 마다 다르게 결과

랜덤은 가능한 구조로 바꿔주기

랜덤부분만 파라미터화시켜 밖으로 빼준다.

  • 기존

          public int move() {
              if (ThreadLocalRandom.current().nextInt(10) >= 4) {
                  position += 1;
              }
              return position;
          }
    
  • 파라미터화시켜 밖으로 빼기

    • 자동으로 사용중이던 부분(랜덤)이 –>메소드 사용처로 이동된다.
           public int move(final int number) {
               if (number >= 4) {
                   position += 1;
               }
               return position;
           }
    
      assertThat(car.move(ThreadLocalRandom.current().nextInt(10))).isEqualTo(0);
    

사용처의 랜덤생성을 특정 상수로 바꾼다.

  • 랜덤은 프로덕션 코드에서만 사용되므로 추출되어 사용처로 옮겨간 랜덤부분을 테스트에서는 상수로 변경한다.

       @Test
       void move_go2() {
           final Car car = new Car();
         
           car.move(5);
           assertThat(car.move(5)).isEqualTo(2);
       }
         
       @Test
       void move_stop() {
           final Car car = new Car();
         
           assertThat(car.move(3)).isEqualTo(0);
       }
       }
    

랜덤 뿐만 아니라 랜덤포함조건자체를 객체의 역할로

  • 객체가 테스트를 위한 랜덤부분 외부 파라미터로 추출 뿐만 아니라

  • if 조건에 따른 다른 역할을 하는 객체로 취급하고 싶다면 if조건 자체를 인터페이스로 추출할 수 있다.

    • 전략패턴은 대체가능한 다른전략을 넣는 부분을 인터페이스 -> 전략메서드로 추출해놓는다.
    1. 바뀔 수 있는 부분을 잡아서 상태에 맞게 메소드로 추출해놓는다.

    2. 인터페이스 XXXXStrategy + 이름만 가진 추상메서드를 만든다.

    3. 특정Strategy의 이름을 정해서 impl구상클래스추출메소드를 복붙해서 구현한다.

    4. 특정전략의 메소드가 사용된 곳마다 —> 다른전략을 허용하는 추상화된 인터페이스 객체인자로 받은 뒤 -> 추상체.추출메소드()호출로 수정한다.

    5. 이제 에러난 부분을 타고가면서, *내부 매개변수 정의에 사용된 추상 인자를 호출하는 외부 인자 자리new 특정전략()를 넣워준다.

    6. 테스트에서 다른전략으로 조건부의 상수인 람다함수로 () -> true를 대입하여 움직임을 확인한다.

확인을 위한 응답 값(조회)이 실제 필요한지 의심해봐야한다

  • 내부 position만 바뀌면 되는데, 테스트를 위해 응답하고 있지는 않은지 의심하자.

응답하는 것도 1가지 일(조회)다. 위치변경(테스트할 기능) + 조회 2가지 일을 하는 move()

  • 포지션을 조회하고 싶다면 또다시 car.move()를 호출해서 확인해야하나? 변하면서 조회되므로 이상하다.

      public int move(final int number) {
          if (number >= 4) {
              position += 1;
          }
          return position;
      }
    
  • 조회는.. 응답 말고도 다른 방법이 있다. 값으로 만드는 객체로 비교

조회 메서드가 아니라면, 테스트 끝난 후, 응답 값 제거리팩토링

  • 메서드2() 복붙 생성하여, 응답이 제거된 메서드2 생성

      public int move(final int number) {
          if (number >= 4) {
              position += 1;
          }
          return position;
      }
        
      public void move2(final int number) {
          if (number >= 4) {
              position += 1;
          }
      }
    
  • Test에서는 assertThat()에 응답 값을 넣어줘야하는데?

      assertThat(car.move(3)).isEqualTo(0);
    

응답 값 대신 [해당객체]를 / expected 예상값 대신 예상값으로 생성한 + equals옵라된 [예상객체]를

  1. 예상값으로 예상객체를 만들어줄 생성자를 추가한다. image-20220224163249798

  2. 기존에는 default 생성자로서, 인자 없이 만들었다. Select None으로 기존 것도 사용가능하게 빈 default 생성자를 따로 정의해주자.

    image-20220224163755370

     public Car() {
     }
    
  3. 생성된 객체로 비교하기 위해, equals, hashCode를 옵라이딩

응답 없는 메소드2로 치환 후, 객체.메서드2호출 - assertThat(객체). (예상객체) 로 비교

  • 이제 전부 응답없는 메소드2로 치환 -> actual 응답 자리에서 빼주기

      @Test
      void move_go() {
          final Car car = new Car();
          car.move2(4);
        
          // 1.actual에는 실제응답 대신, 객체를
          assertThat(car).isEqualTo(new Car(1)); // 2. 예상값으로 만든 예상객체
      }
        
    
  • 기존 assertThat( 응답값 ) .isEqualsTo ( 예상값)

      assertThat(car.move(4)).isEqualTo(0);
    
  • assertThat( 사용객체 ) .isEqualsTo ( 예상객체)

     ar.move2(4);
        
     assertThat(car).isEqualTo(new Car(1));
    

생성자도… 테스트를 위한 생성자인지 고민 -> 그렇다면 .getPosition() 대신 .position()으로 마무리?

  • get이라는 네이밍을 빼서 내부에 position이 있는지 모르게 하는게 핵심

외부추출랜덤은 상위 객체 테스트를 막는다

  • 자동차.move()의 내부 랜덤 추출 -> 자동차.move(랜덤사용) -> 자동차.move(랜덤사용)을 호출하는 자동차경주가 내부 랜덤을 포함하게 됨.

상위 객체(관리자) Test -> List< 테스트끝난객체> 를 받도록 생성 -> 돌면서 시켜야함.

  1. 단일 테스트가 끝난 단일객체에 대해서 일급 컬렉션 생성 전 LIst<단일>을 사용 == 세팅by생성자( List<단일객체> )하는 상위객체, 관리자객체를 Test서부터 만든다.

    • 필요한 것은 set을 생성자에서 받는다.
    • 다 못받으면, 필요기능의 메서드 파라미터로
     @Test
     void name() {
         final List<Car> cars = Arrays.asList(new Car(), new Car());
        
         final RacingGame racingGame = new RacingGame(cars);
     }
    
     public class RacingGame {
         public RacingGame(final List<Car> cars) {
         }
     }
    
     public class RacingGame {
         private final List<Car> cars;
        
         public RacingGame(final List<Car> cars) {
             this.cars = cars;
         }
     }
    
  2. 관리자=상위객체에서 메서드() -> list를 돌며 단일객체 메서드 시키기

     public class RacingGame {
         private final List<Car> cars;
        
         public RacingGame(final List<Car> cars) {
             this.cars = cars;
         }
        
         public void start() {
             cars.forEach( car -> car.move(ThreadLocalRandom.current().nextInt(10)));
         }
     }
    
  3. Car만 테스트는 가능하고, 그 상위객체인 RacingGame은 테스트가 불가능한 상태다.

    • 원한다면 다시 랜덤부분을 파라미터로 추출하면 된다.
    • 어디까지 테스트가능 구조로 할지는 자기가 선택하는 것
    • 상위객체도 테스트 가능한 구조로 만들고 싶다면, 파라미터화시켜 외부로 빼면 된다.

가장 끝단의 객체를 찾고, 테스트 불가 부분을 밖으로 뺀다.

image-20220224160243276

  • 한단계 더 올리면, RacingGame도 테스트가 가능해진다.

    image-20220224160301143

  • 랜덤, 특정 날짜에만 작동 등이 테스트하기 힘든 코드다.

    • 외부로 분리해서 주입한다.

image-20220224160337461