TDD 2 자동차경주(객체Dto)
기능구현목록, 응답+예상값->객체+예상객체로 테스트
📜 제목으로 보기
- 기능 요구사항 복붙후 정리
- TDD 시작
- case 추가(누적 -> 응답 expected 변경 2)
- case 추가(조건 따라 stop -> expected는 stop일때 나와야하는 값으로 미리 넣어두기 0)
- 랜덤은 가능한 구조로 바꿔주기
- 확인을 위한 응답 값(조회)이 실제 필요한지 의심해봐야한다
- 외부추출랜덤은 상위 객체 테스트를 막는다
- 실패하는 코드를 짜는 이유?
- 통과하는 코드를 짜려고 하면, 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); } }
- 움직였을 때, 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조건 자체를 인터페이스
로 추출할 수 있다.-
전략패턴
은 대체가능한 다른전략을 넣는 부분을 인터페이스 -> 전략메서드로 추출해놓는다.
-
바뀔 수 있는 부분을 잡아서 상태에 맞게 메소드로 추출해놓는다.
-
인터페이스 XXXXStrategy
+이름만 가진 추상메서드
를 만든다. -
특정Strategy
의 이름을 정해서impl구상클래스
를추출메소드를 복붙
해서 구현한다. -
특정전략의 메소드
가 사용된 곳마다 —> 다른전략을 허용하는추상화된 인터페이스 객체
를인자
로 받은 뒤 ->추상체.추출메소드()
호출로 수정한다. -
이제 에러난 부분을 타고가면서, *
내부 매개변수
정의에 사용된추상 인자
를 호출하는외부 인자 자리
에new 특정전략()
를 넣워준다. -
테스트에서 다른전략으로
조건부의 상수
인 람다함수로() -> 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옵라된 [예상객체]를
-
예상값으로
예상객체
를 만들어줄생성자를 추가
한다. -
기존에는 default 생성자로서, 인자 없이 만들었다.
Select None
으로 기존 것도 사용가능하게 빈 default 생성자를 따로 정의해주자.public Car() { }
-
생성된 객체로 비교하기 위해, 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< 테스트끝난객체> 를 받도록 생성 -> 돌면서 시켜야함.
-
단일 테스트가 끝난
단일객체
에 대해서일급 컬렉션 생성 전 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; } }
-
관리자=상위객체에서 메서드() -> 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))); } }
-
Car만 테스트는 가능하고, 그 상위객체인 RacingGame은 테스트가 불가능한 상태다.
- 원한다면 다시 랜덤부분을 파라미터로 추출하면 된다.
- 어디까지 테스트가능 구조로 할지는 자기가 선택하는 것
- 상위객체도 테스트 가능한 구조로 만들고 싶다면, 파라미터화시켜 외부로 빼면 된다.
가장 끝단의 객체를 찾고, 테스트 불가 부분을 밖으로 뺀다.
-
한단계 더 올리면, RacingGame도 테스트가 가능해진다.
-
랜덤, 특정 날짜에만 작동 등이
테스트하기 힘든 코드
다.- 외부로 분리해서 주입한다.