[TDD] 자동차 경주 게임

0_0_yoon·2021년 12월 4일
0

TDD

목록 보기
3/3
post-thumbnail

📌 설계

📍 도메인 모델

  • 자동차 레이싱 게임을 기반으로 구조 설계
  • 비즈니스 규칙은 토큰 게임기 작동 규칙으로 함.
  • 도메인 모델에서 추출한 객체: 게임기, 트랙, 레코더, 자동차, 엔진, 토큰

📍 기능목록

  • 자동차들 이름을 입력받는다.

    • 쉼표를 기준으로 구분
    • 자동차 이름은 5글자 이하
  • 횟수 토큰을 받는다.

    • 숫자만 가능하다.
  • 게임을 시작한다.

  • 자동차가 전진 한다.

    • 숫자가 4 이상이면 전진한다.
  • 레이싱 트랙을 보여준다.

  • 우승자를 보여준다.

📍 예외발생 케이스

  • 자동차 이름 전체 입력값이 빈값일 경우

  • 자동차 이름 전체 입력값이 "," 로 시작하는 경우

  • 자동차 이름 중 공백만 있을 경우

  • 자동차 이름 중 중복이 있을 경우

  • 자동차 한대 이름의 길이가 5글자가 넘는 경우

  • 자동차 이름이 한개일 경우

  • 횟수 입력값이 숫자가 아닐 경우

  • 횟수 입력값이 빈값일 경우

  • 횟수 입력값이 0일 경우

📌 랜덤값을 어떻게 테스트 할 것인가?

핵심 기능인 랜덤값에 따른 자동차 전진을 어떻게 테스트 할까? 단순하게 테스트하기 어렵다면 분리시키자.

//코드 일부만 가져왔다.
public class Cars {
   public void move(Engines engines) {
        IntStream.range(START_INDEX, cars.size()).forEach(index -> {
            if (engines.canOperate(index)) {
                cars.get(index).move();
            }
        });
    }
}

public class Engines {
    private final List<Engine> engines;

    public Engines(String value) {
        this.engines = value.chars().map(Character::getNumericValue).mapToObj(Engine::new).collect(Collectors.toList());
    }

    public boolean canOperate(int index) {
        return engines.get(index).canOperate();
    }
}

public class Engine {
    public static final int FORWARD_THRESHOLD_NUMBER = 4;

    private final int number;

    public Engine(int number) {
        this.number = number;
    }

    public boolean canOperate() {
        return number >= FORWARD_THRESHOLD_NUMBER;
    }
}

위와 같이 전진을 결정하는 역할을 Engine 객체에게 위임했다. 이 Engine 객체는 생성자를 통해 외부에서 값을 받아 멤버필드를 초기화하고 canOperate 메서드를 통해 전진을 결정하는 역할을 수행한다.

public class CarsTest {
    public static final String NEW_LINE = System.lineSeparator();

    Cars cars;

    @BeforeEach
    void setUp() {
        this.cars = Cars.createByNames("1,2,3");
    }

    @DisplayName("경기 결과를 문자열로 반환 테스트")
    @Test
    void getGameRecord() {
        cars.move(new Engines("000"));
        assertThat(cars.getGameRecord()).isEqualTo("1 : " + NEW_LINE + "2 : " + NEW_LINE + "3 : ");
        cars.move(new Engines("444"));
        assertThat(cars.getGameRecord()).isEqualTo("1 : -" + NEW_LINE + "2 : -" + NEW_LINE + "3 : -");
        cars.move(new Engines("349"));
        assertThat(cars.getGameRecord()).isEqualTo("1 : -" + NEW_LINE + "2 : --" + NEW_LINE + "3 : --");
    }

    @DisplayName("우승자 문자열로 반환 테스트")
    @Test
    void getWinner() {
        cars.move(new Engines("012"));
        assertThat(cars.getWinner()).isEqualTo("1 2 3");
        cars.move(new Engines("049"));
        assertThat(cars.getWinner()).isEqualTo("2 3");
        cars.move(new Engines("034"));
        assertThat(cars.getWinner()).isEqualTo("3");
    }
}

랜덤값을 아예 배제하고 위와 같이 생성자를 통해 원하는 값을 넣어 예상 가능한 결과를 테스트 함으로써 기존에 랜덤값과 연결돼 있던 모든 기능들을 테스트할 수 있게 됐다.

📌 예외발생 테스트하기

모든 구현을 마치고 실제 실행하는 과정에서 예상치 못한 예외를 발견했다. 그건 바로 입력값을 숫자로 변환하는 과정에서 발생한 NumberFormatException 이였다. 분명 이미 테스트 케이스에서 유효성 검증을 마쳤고 같은 시나리오로 진행 한 거였다.

//기존 테스트 코드
@DisplayName("토큰 생성시 유효하지 않은 값에 대한 예외발생")
@ParameterizedTest
@ValueSource(strings = {"", "a", "!", "%"})
void tokenFromInvalidValue(String value) {
    assertThatThrownBy(() -> Token.from(value)).isInstanceOf(IllegalArgumentException.class)
}

문제의 원인은 테스트 코드였다. NumberFormatException의 상위 클래스인 IllegalArgumentException의 발생 유무만을 테스트했기 때문에 당연히 통과됐고 나는 유효성이 검증됐다고 생각한 것이다.

@DisplayName("토큰 생성시 유효하지 않은 값에 대한 예외발생")
@ParameterizedTest
@ValueSource(strings = {"", "a", "!", "%"})
void tokenFromInvalidValue(String value) {
    assertThatThrownBy(() -> Token.from(value)).isInstanceOf(IllegalArgumentException.class)
        .hasMessage(ERROR_MESSAGE);
}

빨리 구현하려다가 되려 시간을 버렸다. 위와 같이 발생 유무뿐만이 아니라 상세하게 테스트 케이스를 작성해서 이런 실수를 줄여나가야겠다.

profile
꾸준하게 쌓아가자

0개의 댓글