public void race() {
for(Car car : cars) {
car.move(movable());
}
}
여기에 남기는 코멘트는 아니고 Cars#race에 남기는 코멘트입니다.
이 메서드는 도메인 객체인 Cars의 메서드임에도 불구하고 테스트가 불가능한 구조인 것 같아요.
Cars에서 랜덤을 분리한다면 테스트가 가능한 구조를 만들 수 있을 거 같은데 한 번 도전해보시는 것도 좋을 거 같네요 :)
이 부분 이해가 잘 안됐습니다! 테스트 하기 불가능한 구조인 것 까지는 이해를 했는데, Cars에서 랜덤을 분리하면 왜 테스트가 가능해지는지 이해가 잘 안되네요.
항상 일정한 값이 나올 수 있게 하는 외부 라이브러리
를 사용하는 방식 역시 전략패턴에 기반하고 있습니다.
전략 패턴을 좀 더 편하게 사용하기 위한 라이브러리라고 생각하시면 좋을 것 같네요.
우테코 레벨 1 미션들에서는 아마 해당 라이브러리 사용의 필요성을 느끼시지 못할 거에요. 가급적 직접 구현해보시길 바랍니다.
지금 코드에서 race()를 호출하면 각 자동차들이 움직였을 수도, 움직이지 않았을 수도 있어서 테스트가 불가능하죠.
이 메서드가 테스트가 되지 않는 이유를 어디서 찾아야할까요?
바로 Cars객체 내부에서 랜덤한 값을 만드는 부분입니다.그럼 어떻게 테스트 가능하게 구현할 수 있을까요?
실제 코드가 동작할 때는 랜덤으로 전진시키고, 테스트 상황에서는 항상 전진시키거나 전진시키지 않게 구현할 수 있으면 테스트가 가능할 것 같아요.이 글을 읽어보고 전략패턴에 대해서 한 번 고민해보시면 방법을 찾을 수도 있을 것 같네요.
전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.
자바의 다형성을 이용하면 구현 가능할 것이라는 느낌이 온다.
우선 인터페이스를 만들어 주자. 자동차 경주 게임에서는 NumberGenerator
인터페이스를 만든다.
NumberGenerator
public interface NumberGenerator {
int generate();
}
이렇게 하고 나면 move()
메서드는 NumberGenerator
를 인자로 받아서, numberGenerator.generate()
의 결과에 따라 판단을 한다.
즉, 실제 앱에서의 generate()
와 테스트 환경에서 통제 가능한 generate()
를 각각 구현해주면 되는 것이다.
RandomNumberGenerator
public class RandomNumberGenerator implements NumberGenerator {
private static final int MAX_RANDOM_VALUE = 10;
private static final Random random = new Random();
@Override
public int generate() {
return random.nextInt(MAX_RANDOM_VALUE);
}
}
이것은 실제 앱에서 사용될 generator이다.
MovableGenerator
public class MovableNumberGenerator implements NumberGenerator {
@Override
public int generate() {
return 4;
}
}
테스트용 generator다. 이 MovableNumberGenerator
를 인자로 전달한다면 항상 move
가 가능할 것이다. 어디까지나 테스트용이므로 테스트 패키지에 넣어야 한다.
NonMovableGenerator
public class NonMovableGenerator implements NumberGenerator {
@Override
public int generate() {
return 3;
}
}
이 NonMovableNumberGenerator
를 인자로 전달한다면 항상 move
가 불가능할 것이다.
이제 테스트가 더 이상 랜덤이 아니다. 뭐를 전달해주냐에 따라 직접 통제 가능하다.
CarTest
class CarsTest {
@DisplayName("각 차들이 모두 움직인다")
@Test
public void race() {
...
cars.race(new MovableNumberGenerator());
assertThat(cars.getParticipantCars())
.filteredOn(car -> car.isSamePosition(nonMoveCar))
.isEmpty();
}
이 패턴을 적용시키며 궁금한 점이 하나 생겼습니다. 지금 제 코드에서는
Cars
의race()
에RandomNumberGenerator
를 인수로 전달하게 구현하였는데요, 이 전달하는 부분은RacingController
입니다. 이러면RacingController
부분에서 테스트 하기 어려워 졌다는 생각이 들었습니다 (RandomNumberGenerator
를 전달하니까). 뭔가 책임의 회피 같은 느낌이 들어서 조금 찜찜한 기분이 남아있습니다.
좋은 질문이에요. 이 코멘트에 대한 답변도 같이 드릴 수 있을 거 같아요. Cars에서 랜덤 객체의 생성을 상위 객체로 넘긴 거 처럼 여기서도 더 상위 객체로 옮기면 됩니다. RacingController의 생성자에서 해당 객체를 받아오면 되겠죠.
Application에서 객체를 생성하는게 이상하다고 느끼실 수도 있을텐데, 이 부분은 이번 단계에서는 넘어가도 좋을 거 같아요. 우테코 레벨2 쯤에 이 문제가 어떻게 해결되는지 확인할 수 있겠네요.
레벨2에서 이 문제를 만난다면 다시 보충해서 적도록 하겠다.