자동차들 이름을 입력받는다.
횟수 토큰을 받는다.
게임을 시작한다.
자동차가 전진 한다.
레이싱 트랙을 보여준다.
우승자를 보여준다.
자동차 이름 전체 입력값이 빈값일 경우
자동차 이름 전체 입력값이 "," 로 시작하는 경우
자동차 이름 중 공백만 있을 경우
자동차 이름 중 중복이 있을 경우
자동차 한대 이름의 길이가 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);
}
빨리 구현하려다가 되려 시간을 버렸다. 위와 같이 발생 유무뿐만이 아니라 상세하게 테스트 케이스를 작성해서 이런 실수를 줄여나가야겠다.