diff --git a/docs/SETP3-README.md b/docs/STEP3-README.md similarity index 95% rename from docs/SETP3-README.md rename to docs/STEP3-README.md index e7b0448cba..02bd3b67d1 100644 --- a/docs/SETP3-README.md +++ b/docs/STEP3-README.md @@ -57,11 +57,11 @@ * [x] Service 계층에 위치한 난수 생성부 분리 * Service 계층에 위치한 난수 생성부로 인해 해당 계층이 테스트 하기 어려운 문제를 개선하기 위해 난수 생성부를 Service 계층 분리 후, 외부에서 생성된 난수를 각 `Car`객체에 주입하여 해당 계층을 테스트 가능하도록 수정 * 난수 생성부 위치 변경 - * AS*IS : `Car` 객체에서 경주 진행 메서드 호출 시, 난수를 생성 - * TO*BE : Car 객체 생성 시, 진행 라운드 만큼 난수 생성 후 Car 객체 필드에 주입 + * AS-IS : `Car` 객체에서 경주 진행 메서드 호출 시, 난수를 생성 + * TO-BE : Car 객체 생성 시, 진행 라운드 만큼 난수 생성 후 Car 객체 필드에 주입 * 각 차량의 자동차 경주 진행 방식 변경 - * AS*IS : 각 차량에서 현재 라운드 진행을 위해 난수 생성 및 생성된 난수에 따른 진행거리 누적 여부 판별 - * TO*BE : 외부에서 각 차량에 진행 라운드 만큼 아미 생성된 난수를 판별하여 진행 거리 누적 여부만 판별 + * AS-IS : 각 차량에서 현재 라운드 진행을 위해 난수 생성 및 생성된 난수에 따른 진행거리 누적 여부 판별 + * TO-BE : 외부에서 각 차량에 진행 라운드 만큼 아미 생성된 난수를 판별하여 진행 거리 누적 여부만 판별 * 로직 변경으로 인해 깨지는 TC 수정 * 생성된 난수 목록 일급 컬렉션 정의 * 진행 라운드 만큼 생성된 난수 목록이 주입되는 일급 컬렉션 diff --git a/docs/STEP4-README.md b/docs/STEP4-README.md index 9c6003b265..ea638adaf6 100644 --- a/docs/STEP4-README.md +++ b/docs/STEP4-README.md @@ -32,3 +32,64 @@ * 우승자 도메인 정의 * 경주 완료 후 자동차 객체 목록을 통해 우승자 객체 생성 및 우승자 목록 추출 * 경주를 완료한 자동차 객체 목록을 통해 우승한 차량의 이름 추출 TC 작성 + +## 코드 리뷰 피드백 내용 정리 +* [x] 별도의 역할 없이 객체를 포장하고 있는 `Cars`일급 컬렉션과 참가한 자동차 목록으로 부터 우승자를 산출하는 `Winners` 일급 컬렉션 역할을 병합 + +* [x] 각 라운드 진행 방식 변경 + * AS-IS : `Car` 객체에 미리 생성된 난수를 판별하여 주행거리를 누적 + * TO-BE : 매 라운드 마다 발생된 난수를 판별한 후, 차량 주행거리를 누적 + * [x] 추상화를 통해 난수에 대한 의존성으로 테스트 하기 어려운 현상 개선 + * [x] 난수 발생 부를 추상화하여 운영 환경에 주입되는 구현체와 테스트 환경에 주입되는 구현체를 별도로 구현한 뒤, 각 실행 환경에 맞는 구현체를 선택하여 주입함으로써 난수로 인해 테스트 하기 어려운 문제 개선 + * 추상화된 난수 생성부 주입을 통한 자동차 경주 진행 + * [x] 자동차 경주를 진행하는 `RacingService` 객체에 추상화된 난수 생성 부의 구현체를 주입하여 생성한 후, 각 라운드 진행 시 운영 환경에서는 실제 난수 생성 구현체가 동작하고, 테스트 환경에서는 난수 발생 시 의도한 수를 반환하는 구현체가 동작할 수 있도록 수정 + * [x] 난수 생성 부 추상화에 따른 `Car`객체에 자동차를 표현하기 어색한 `RandomNumbers` 필드 제거 + +* 테스트 표현 개선 + * [x] 도메인 로직을 활용한 테스트 결과 검증 부를 하드코딩된 값으로 변경 + * 테스트 검증 부가 도메인 로직을 참조하고 있음에 따라 로직 변경 시 테스트로서의 역할을 제대로 수행할 수 없는 문제점 개선을 위해 테스트 검증 부를 하드코딩된 값으로 변경 + * AS-IS : + ```kotlin + given.winnerNames() shouldBe "${두_번째_차량.name}, ${세_번째_차량.name}" + ``` + * TO-BE : + ```kotlin + given.winnerNames() shouldBe "두 번째 차량, 세 번째 차량" + ``` + * [x] (테스트 픽스처 활용으로 인해 테스트 코드가 결합되는 현상 개선)[https://jojoldu.tistory.com/611] + * 테스트 픽스처로 인해 테스트 코드가 결합되어 테스트 코드 유지보수가 복잡해지는 현상 개선을 위해, 각 테스트에 필요한 fixture 생성부를 private 메서드로 구성 + +## 2차 코드 리뷰 피드백 내용 정리 +* Car 도메인 수정 + * [x] `variable`로 선언된 `distance`(누적 주행 거리) 속성의 외부 변경을 제한하기 위해 누락된 `private` 접근 제어자 추가 + * [x] 미사용 상수 제거 및 `distance` 속성의 초기값 상수 추가 + * [x] 경주가 완료된 차량의 용이한 테스트를 위해 `Car` 도메인의 `distance` 속성의 초기값 설정이 가능하도록 수정 + * [x] 우승자 산출 방식 변경 : 해당 차량의 누적 주행거리가 최대값인지를 확인하는 함수 작성 + * AS-IS : 우승자 산출을 위해 경주에 기존 `Cars` 객체에서 각 원소(Car)를 순회하며 distance 속성에 접근하여 최대 누적 주행거리를 비교 + * TO-BE : `Cars` 객체에서 각 원소(Car)를 순회하며 누적 주행거리가 최대값인 원소를 추출 + * [x] 우승자 산출을 위한 `Cars` 도메인 TC의 Car.race() 호출 부 개선 + * 우승자 산출 테스트를 위해 Car.race()를 호출하여 누적 주행거리를 설정하는 테스트 방식을 `Car` 도메인의 `distance` 속성 초기값 설정을 통해 제거하여 테스트 간소화 + * 경주를 완료한 차량 생성을 위해 더 이상 불필요한 `test fixture` 생성 함수 제거 + +* Cars 도메인 수정 + * [x] private 생성자의 `elements` 프로퍼티를 반환하는 getter 제거하고, 해당 프로퍼티를 public 으로 변경 + * [x] ','를 기준으로 우승자 이름을 묶어내는 `Cars` 도메인의 함수를 View 계층으로 이동 + * [x] Service 계층에서 `Cars` 도메인의 각 원소(Car)에 접근하여 경주를 진행하는 방식에서 Cars 도메인에 메세지를 보내어 각 원소(Car)의 race() 함수를 호출함으로써 도메인이 메세지를 받아 직접 로직을 처리 하도록 수정 + +* [x] 명확하지 않은 난수 생성기 인터페이스 이름 변경 + * AS-IS : RandomNumber + * TO-BE : NumberGenerator + +* [x] 명확하지 않은 Given/When 테스트 표현 개선 + +* 도메인 계층과 서비스 계층에 구현된 출력 부를 View 계층으로 이동 + * AS-IS : `Cars` 도메인에서 직접 각 라운드 결과를 출력하고, `RacingService` 계층에서 우승자를 출력 + * TO-BE : + * [x] 각 `Car` 객체가 해당 라운드의 경주를 완료 하면 각 라운드 결과를 기록한 객체(RoundResult)를 반환 + * [x] `Cars` 객체는 `Car` 객체로부터 각 라운드 결과 객체를 반환받아 합산한 후, 전체 라운드 결과 객체(RoundResults)를 반환 + * [x] `RacingService` 계층은 반환받은 전체 라운드 결과 객체를 출력부에 전달하여 로직과 출력부를 분리 + * [x] 각 라운드 결과 객체에 현재 `Car` 객체의 상태값을 저장하기 위한 DeepCopy 함수 구현 + * [x] Cars 도메인 계층에서 출력부를 분리함에 따라, 출력부에서 접근하고 있던 `Cars` 도메인의 `elements` 프로퍼티의 접근 제한자를 `private`에서 `public`으로 변경 + * 개선 사항 : + * 출력을 위해 public 으로 열려있는 도메인 계층의 프로퍼티의 접근 제한자를 private 으로 변경 + * 서비스 계층과 도메인 계층은 자동차 경주만을 위한 코드가 작성되고, 출력 부는 로직이 완료 된 후 반환된 객체를 넘겨 받아 결과를 출력함으로써 출력부에 대한 로직 실행부의 의존성 제거 diff --git a/src/main/kotlin/step3/racingcar/controller/RacingCarController.kt b/src/main/kotlin/step3/racingcar/controller/RacingCarController.kt index 288823242b..806187f7c6 100644 --- a/src/main/kotlin/step3/racingcar/controller/RacingCarController.kt +++ b/src/main/kotlin/step3/racingcar/controller/RacingCarController.kt @@ -4,19 +4,20 @@ import step3.racingcar.domain.Cars import step3.racingcar.domain.PlayInfo import step3.racingcar.service.RacingCarService import step3.racingcar.utils.CarGenerator -import step3.racingcar.utils.RandomNumberGenerator.generateRandomNumberToCarByRound +import step3.racingcar.utils.RandomNumberGenerator import step3.racingcar.view.InputView.Companion.inputJoinerCarsGuideMessagePrinter import step3.racingcar.view.InputView.Companion.inputRoundCountGuideMessagePrinter +import step3.racingcar.view.ResultView class RacingCarController { - private val racingCarService: RacingCarService = RacingCarService() + private val racingCarService: RacingCarService = RacingCarService(RandomNumberGenerator()) fun gameStart() { val carNames = inputJoinerCarsGuideMessagePrinter() val totalRound = inputRoundCountGuideMessagePrinter() val cars = Cars.of(CarGenerator.generate(carNames)) - generateRandomNumberToCarByRound(cars, totalRound) val playInfo = PlayInfo(cars, totalRound) - racingCarService.play(playInfo) + val playResult = racingCarService.play(playInfo) + ResultView.printResult(playResult) } } diff --git a/src/main/kotlin/step3/racingcar/domain/Car.kt b/src/main/kotlin/step3/racingcar/domain/Car.kt index 5cead021c0..6593b8d277 100644 --- a/src/main/kotlin/step3/racingcar/domain/Car.kt +++ b/src/main/kotlin/step3/racingcar/domain/Car.kt @@ -1,20 +1,16 @@ package step3.racingcar.domain -private const val CAR_ID_DELIMITER = "-" private const val MOVE_CRITERIA = 4 +private const val INITIAL_DISTANCE = 0 -class Car(val name: String) { - private var randomNumbers: RandomNumbers = RandomNumbers() - var distance = 0 - - fun addRandomNumber(randomNumber: Int) = randomNumbers.add(randomNumber) - - fun race(currentRoundIndex: Int) { - val randomNumberByCurrentRound: Int = randomNumbers[currentRoundIndex] - if (isMove(randomNumberByCurrentRound)) { +class Car(val name: String, var distance: Int = INITIAL_DISTANCE) { + fun race(randomNumber: Int) { + if (isMove(randomNumber)) { distance++ } } + fun isMaximumDistance(maxDistance: Int): Boolean = distance == maxDistance private fun isMove(randomNumber: Int): Boolean = randomNumber >= MOVE_CRITERIA + fun copy(): Car = Car(name, distance) } diff --git a/src/main/kotlin/step3/racingcar/domain/Cars.kt b/src/main/kotlin/step3/racingcar/domain/Cars.kt index 57ab2f6e2b..90bd8b9781 100644 --- a/src/main/kotlin/step3/racingcar/domain/Cars.kt +++ b/src/main/kotlin/step3/racingcar/domain/Cars.kt @@ -1,11 +1,29 @@ package step3.racingcar.domain class Cars private constructor(private val elements: List) { - fun elements(): List = elements fun size(): Int = elements.size - operator fun get(index: Int) = elements[index] + + operator fun get(index: Int): Car = elements[index] + + fun race(numberGenerator: NumberGenerator): RoundResult { + val roundResult = RoundResult() + elements.forEach { + it.race(numberGenerator.value()) + roundResult.add(it) + } + return roundResult + } + + fun winnerNames(): List { + val maxDistance = findMaxDistance() + return elements + .filter { it.isMaximumDistance(maxDistance) } + .map { it.name } + } + + private fun findMaxDistance(): Int = elements.maxOf { it.distance } companion object { - fun of(elements: List) = Cars(elements) + fun of(elements: List): Cars = Cars(elements) } } diff --git a/src/main/kotlin/step3/racingcar/domain/NumberGenerator.kt b/src/main/kotlin/step3/racingcar/domain/NumberGenerator.kt new file mode 100644 index 0000000000..6a692a2e6f --- /dev/null +++ b/src/main/kotlin/step3/racingcar/domain/NumberGenerator.kt @@ -0,0 +1,10 @@ +package step3.racingcar.domain + +interface NumberGenerator { + fun value(): Int + + companion object { + const val RANGE_START = 1 + const val RANGE_END = 9 + } +} diff --git a/src/main/kotlin/step3/racingcar/domain/RandomNumbers.kt b/src/main/kotlin/step3/racingcar/domain/RandomNumbers.kt deleted file mode 100644 index 4dc1428322..0000000000 --- a/src/main/kotlin/step3/racingcar/domain/RandomNumbers.kt +++ /dev/null @@ -1,7 +0,0 @@ -package step3.racingcar.domain - -class RandomNumbers { - private var elements: MutableList = mutableListOf() - fun add(randomNumber: Int) = elements.add(randomNumber) - operator fun get(index: Int): Int = elements[index] -} diff --git a/src/main/kotlin/step3/racingcar/domain/RoundResult.kt b/src/main/kotlin/step3/racingcar/domain/RoundResult.kt new file mode 100644 index 0000000000..bb2ee7914a --- /dev/null +++ b/src/main/kotlin/step3/racingcar/domain/RoundResult.kt @@ -0,0 +1,9 @@ +package step3.racingcar.domain + +class RoundResult { + private val elements: MutableList = mutableListOf() + + operator fun get(index: Int): Car = elements[index] + fun add(car: Car) = elements.add(car.copy()) + fun size(): Int = elements.size +} diff --git a/src/main/kotlin/step3/racingcar/domain/RoundResults.kt b/src/main/kotlin/step3/racingcar/domain/RoundResults.kt new file mode 100644 index 0000000000..e2e013e5f6 --- /dev/null +++ b/src/main/kotlin/step3/racingcar/domain/RoundResults.kt @@ -0,0 +1,12 @@ +package step3.racingcar.domain + +class RoundResults private constructor(val totalRound: Int, val cars: Cars) { + private val elements: MutableList = mutableListOf() + + operator fun get(index: Int) = elements[index] + fun add(roundResult: RoundResult) = elements.add(roundResult) + + companion object { + fun of(playInfo: PlayInfo): RoundResults = RoundResults(playInfo.totalRound, playInfo.cars) + } +} diff --git a/src/main/kotlin/step3/racingcar/domain/Winners.kt b/src/main/kotlin/step3/racingcar/domain/Winners.kt deleted file mode 100644 index 425e624a4a..0000000000 --- a/src/main/kotlin/step3/racingcar/domain/Winners.kt +++ /dev/null @@ -1,22 +0,0 @@ -package step3.racingcar.domain - -class Winners private constructor(cars: Cars) { - val names: String = formatToWinnerNames(cars, findMaxDistance(cars)) - - private fun findMaxDistance(cars: Cars) = cars.elements().maxOf { it.distance } - - private fun formatToWinnerNames(cars: Cars, maxDistance: Int): String { - - return findWinnerNames(cars, maxDistance).joinToString(WINNER_NAME_JOINING_SEPARATOR) - } - - private fun findWinnerNames(cars: Cars, maxDistance: Int) = - cars.elements() - .filter { it.distance == maxDistance } - .map { it.name } - - companion object { - private const val WINNER_NAME_JOINING_SEPARATOR = ", " - fun of(cars: Cars): Winners = Winners(cars) - } -} diff --git a/src/main/kotlin/step3/racingcar/service/RacingCarService.kt b/src/main/kotlin/step3/racingcar/service/RacingCarService.kt index ed23e09a87..add4a8bf2b 100644 --- a/src/main/kotlin/step3/racingcar/service/RacingCarService.kt +++ b/src/main/kotlin/step3/racingcar/service/RacingCarService.kt @@ -1,23 +1,27 @@ package step3.racingcar.service +import step3.racingcar.domain.Car import step3.racingcar.domain.Cars +import step3.racingcar.domain.NumberGenerator import step3.racingcar.domain.PlayInfo -import step3.racingcar.domain.Winners -import step3.racingcar.view.ResultView.Companion.printRoundResult -import step3.racingcar.view.ResultView.Companion.printWinner +import step3.racingcar.domain.RoundResult +import step3.racingcar.domain.RoundResults -class RacingCarService { - fun play(playInfo: PlayInfo) { +class RacingCarService(private val numberGenerator: NumberGenerator) { + fun play(playInfo: PlayInfo): RoundResults { + val roundResults = RoundResults.of(playInfo) repeat(playInfo.totalRound) { - playEachRound(it, playInfo.cars) + val playEachRoundAndReturn = playEachRound(playInfo.cars) + roundResults.add(playEachRoundAndReturn) } - printWinner(Winners.of(playInfo.cars)) + return roundResults } - private fun playEachRound(currentRoundIndex: Int, cars: Cars) { - cars.elements().forEach { - it.race(currentRoundIndex) - } - printRoundResult(currentRoundIndex, cars) + fun playEachRound(cars: Cars): RoundResult = + cars.race(numberGenerator) + + fun playEachRoundByCar(car: Car) { + val randomNumber = numberGenerator.value() + car.race(randomNumber) } } diff --git a/src/main/kotlin/step3/racingcar/utils/RandomNumberGenerator.kt b/src/main/kotlin/step3/racingcar/utils/RandomNumberGenerator.kt index ea485c48d6..88d951ef47 100644 --- a/src/main/kotlin/step3/racingcar/utils/RandomNumberGenerator.kt +++ b/src/main/kotlin/step3/racingcar/utils/RandomNumberGenerator.kt @@ -1,23 +1,11 @@ package step3.racingcar.utils -import step3.racingcar.domain.Car -import step3.racingcar.domain.Cars +import step3.racingcar.domain.NumberGenerator +import step3.racingcar.domain.NumberGenerator.Companion.RANGE_END +import step3.racingcar.domain.NumberGenerator.Companion.RANGE_START -object RandomNumberGenerator { - private const val RANGE_START = 1 - private const val RANGE_END = 9 - - fun generateRandomNumberToCarByRound(cars: Cars, totalRound: Int) { - cars.elements().forEach { - generateRandomNumberToEachCar(it, totalRound) - } - } +class RandomNumberGenerator : NumberGenerator { + override fun value(): Int = generate() private fun generate(): Int = (RANGE_START..RANGE_END).random() - - private fun generateRandomNumberToEachCar(car: Car, totalRound: Int) { - repeat(totalRound) { - car.addRandomNumber(generate()) - } - } } diff --git a/src/main/kotlin/step3/racingcar/view/ResultView.kt b/src/main/kotlin/step3/racingcar/view/ResultView.kt index 42c6b88454..a263d789f6 100644 --- a/src/main/kotlin/step3/racingcar/view/ResultView.kt +++ b/src/main/kotlin/step3/racingcar/view/ResultView.kt @@ -2,17 +2,28 @@ package step3.racingcar.view import step3.racingcar.domain.Car import step3.racingcar.domain.Cars -import step3.racingcar.domain.Winners +import step3.racingcar.domain.RoundResult +import step3.racingcar.domain.RoundResults class ResultView { companion object { private const val SCORE_BAR = "-" private const val ROUND_COMPLETE_GUIDE_MESSAGE_FORMAT = "%d 라운드가 종료되었습니다." private const val WINNER_GUIDE_MESSAGE_FORMAT = "%s 가 최종 우승했습니다." + private const val WINNER_NAME_JOINING_SEPARATOR = ", " - fun printRoundResult(currentRoundIndex: Int, cars: Cars) { - roundCompleteGuideMessage(currentRoundIndex) - cars.elements().forEach(::printEachCarRoundResult) + fun printResult(roundResults: RoundResults) { + repeat(roundResults.totalRound) { + roundCompleteGuideMessage(it) + printRoundResult(roundResults[it]) + } + printWinner(roundResults.cars) + } + + private fun printRoundResult(roundResult: RoundResult) { + repeat(roundResult.size()) { + printEachCarRoundResult(roundResult[it]) + } } private fun roundCompleteGuideMessage(currentRoundIndex: Int) { @@ -20,9 +31,8 @@ class ResultView { println(ROUND_COMPLETE_GUIDE_MESSAGE_FORMAT.format(currentRoundIndex.plus(1))) } - private fun printEachCarRoundResult(car: Car) { + private fun printEachCarRoundResult(car: Car) = println("${car.name} : ${distanceToScore(car.distance)}") - } private fun distanceToScore(distance: Int): StringBuilder { val result: StringBuilder = StringBuilder() @@ -32,9 +42,13 @@ class ResultView { return result } - fun printWinner(winners: Winners) { + private fun printWinner(cars: Cars) { println() - println(WINNER_GUIDE_MESSAGE_FORMAT.format(winners.names)) + val formattedWinnerNames = formatToWinnerNames(cars.winnerNames()) + println(WINNER_GUIDE_MESSAGE_FORMAT.format(formattedWinnerNames)) } + + private fun formatToWinnerNames(winnerNames: List): String = + winnerNames.joinToString(WINNER_NAME_JOINING_SEPARATOR) } } diff --git a/src/test/kotlin/step3/racingcar/domain/CarTest.kt b/src/test/kotlin/step3/racingcar/domain/CarTest.kt index 42a8e0a470..3e34b525ef 100644 --- a/src/test/kotlin/step3/racingcar/domain/CarTest.kt +++ b/src/test/kotlin/step3/racingcar/domain/CarTest.kt @@ -2,53 +2,47 @@ package step3.racingcar.domain import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe -import step3.racingcar.fixture.CarFixtureGenerator class CarTest : BehaviorSpec({ - given("경주에 참가하는 자동차 한대에 4이상의 숫자가 주어지고") { - val currentRoundIndex = 0 - val givenCar = CarFixtureGenerator.난수를_가지는_차량_생성("참가 차량", 4) + given("차량 전진 : 경주에 참가하는 자동차 한대에") { + val givenCar = Car("참가 차량") - `when`("경주를 진행하면") { - givenCar.race(currentRoundIndex) - then("현재 차량의 주행거리를 1만큼 누적한다.") { + `when`("4이상의 숫자로 경주를 진행하면") { + givenCar.race(4) + then("현재 차량의 주행거리는 1만큼 누적된다.") { givenCar.distance shouldBe 1 } } } - given("경주에 참가하는 자동차 한대에 3이하의 숫자가 주어지고") { - val currentRoundIndex = 0 - val givenCar = CarFixtureGenerator.난수를_가지는_차량_생성("참가 차량", 3) + given("차량 멈춤 : 경주에 참가하는 자동차 한대에") { + val givenCar = Car("참가 차량") - `when`("경주를 진행하면") { - givenCar.race(currentRoundIndex) + `when`("3이하의 숫자로 경주를 진행하면") { + givenCar.race(3) then("현재 차량의 주행거리는 누적되지 않는다.") { givenCar.distance shouldBe 0 } } } - given("경주에 참가하는 자동차 한대가 주어지고") { + given("경주에 참가하는 자동차 한대에") { val car = Car("참가 차량") - `when`("첫번째 라운드에 4가 주어지면") { - car.addRandomNumber(4) - car.race(0) + `when`("첫번째 라운드에 4이상의 숫자로 경주를 진행하면") { + car.race(4) then("차량 전진 횟수가 1 증가한다.") { car.distance shouldBe 1 } } - `when`("두번째 라운드에 3이 주어지면") { - car.addRandomNumber(3) - car.race(1) + `when`("두번째 라운드에 3이하의 숫자로 경주를 진행하면") { + car.race(3) then("차량의 전진 횟수는 증가하지 않는다.") { car.distance shouldBe 1 } } - `when`("세번째 라운드에 6이 주어지면") { - car.addRandomNumber(6) - car.race(2) + `when`("세번째 라운드에 4이상의 숫자로 경주를 진행하면") { + car.race(6) then("차량 전진 횟수가 1 증가한다.") { car.distance shouldBe 2 } diff --git a/src/test/kotlin/step3/racingcar/domain/CarsTest.kt b/src/test/kotlin/step3/racingcar/domain/CarsTest.kt new file mode 100644 index 0000000000..de13b87caa --- /dev/null +++ b/src/test/kotlin/step3/racingcar/domain/CarsTest.kt @@ -0,0 +1,20 @@ +package step3.racingcar.domain + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactly + +internal class CarsTest : BehaviorSpec({ + given("경주를 완료한 자동차 객체 목록이 주어지고") { + val 첫_번째_차량 = Car("첫 번째 차량", 2) + val 두_번째_차량 = Car("두 번째 차량", 3) + val 세_번째_차량 = Car("세 번째 차량", 3) + + `when`("우승자 객체를 생성하면") { + val 참가_차량_목록 = listOf(첫_번째_차량, 두_번째_차량, 세_번째_차량) + val given = Cars.of(참가_차량_목록) + then("우승한 차량들의 이름을 반환한다.") { + given.winnerNames().shouldContainExactly("두 번째 차량", "세 번째 차량") + } + } + } +}) diff --git a/src/test/kotlin/step3/racingcar/domain/RoundResultTest.kt b/src/test/kotlin/step3/racingcar/domain/RoundResultTest.kt new file mode 100644 index 0000000000..231771489c --- /dev/null +++ b/src/test/kotlin/step3/racingcar/domain/RoundResultTest.kt @@ -0,0 +1,17 @@ +package step3.racingcar.domain + +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldNotBe + +class RoundResultTest : StringSpec({ + "각 라운드의 경주 결과를 기록하는 객체 생성 시, 현재 시점의 Car 객체 정보를 DeepCopy 한다." { + val 첫번째_차량 = Car("첫번째 차량", 3) + val 두번째_차량 = Car("두번째 차량", 2) + val roundResult = RoundResult() + roundResult.add(첫번째_차량) + roundResult.add(두번째_차량) + + roundResult[0] shouldNotBe 첫번째_차량 + roundResult[1] shouldNotBe 두번째_차량 + } +}) diff --git a/src/test/kotlin/step3/racingcar/domain/WinnersTest.kt b/src/test/kotlin/step3/racingcar/domain/WinnersTest.kt deleted file mode 100644 index 5f840b88fb..0000000000 --- a/src/test/kotlin/step3/racingcar/domain/WinnersTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package step3.racingcar.domain - -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.shouldBe -import step3.racingcar.fixture.CarFixtureGenerator - -internal class WinnersTest : BehaviorSpec({ - given("경주를 완료한 자동차 객체 목록이 주어지고") { - val 첫_번째_차량 = CarFixtureGenerator.경주를_완료한_차량_생성("첫 번째 차량", 1, 1, 1, 4, 4) - val 두_번째_차량 = CarFixtureGenerator.경주를_완료한_차량_생성("두 번째 차량", 1, 1, 4, 4, 4) - val 세_번째_차량 = CarFixtureGenerator.경주를_완료한_차량_생성("세 번째 차량", 4, 4, 4, 1, 1) - - `when`("우승자 객체를 생성하면") { - val joinerCars = Cars.of(listOf(첫_번째_차량, 두_번째_차량, 세_번째_차량)) - val given = Winners.of(joinerCars) - then("우승한 차량들의 이름을 반환한다.") { - given.names shouldBe "${두_번째_차량.name}, ${세_번째_차량.name}" - } - } - } -}) diff --git a/src/test/kotlin/step3/racingcar/fixture/CarFixtureGenerator.kt b/src/test/kotlin/step3/racingcar/fixture/CarFixtureGenerator.kt deleted file mode 100644 index 5c8c2dad3d..0000000000 --- a/src/test/kotlin/step3/racingcar/fixture/CarFixtureGenerator.kt +++ /dev/null @@ -1,26 +0,0 @@ -package step3.racingcar.fixture - -import step3.racingcar.domain.Car - -class CarFixtureGenerator { - companion object { - fun 난수를_가지는_차량_생성(carName: String, vararg randomNumbers: Int): Car { - val car = Car(carName) - randomNumbers.forEach { - car.addRandomNumber(it) - } - return car - } - - fun 경주를_완료한_차량_생성(carName: String, vararg randomNumbers: Int): Car { - val car = Car(carName) - randomNumbers.forEach { - car.addRandomNumber(it) - } - repeat(randomNumbers.size) { - car.race(it) - } - return car - } - } -} diff --git a/src/test/kotlin/step3/racingcar/fixture/TestRandomNumberGenerator.kt b/src/test/kotlin/step3/racingcar/fixture/TestRandomNumberGenerator.kt new file mode 100644 index 0000000000..e261771a1c --- /dev/null +++ b/src/test/kotlin/step3/racingcar/fixture/TestRandomNumberGenerator.kt @@ -0,0 +1,11 @@ +package step3.racingcar.fixture + +import step3.racingcar.domain.NumberGenerator + +class TestRandomNumberGenerator(private val testValue: Int = DEFAULT_TEST_VALUE) : NumberGenerator { + override fun value(): Int = testValue + + companion object { + private const val DEFAULT_TEST_VALUE = 1 + } +} diff --git a/src/test/kotlin/step3/racingcar/service/RacingCarServiceTest.kt b/src/test/kotlin/step3/racingcar/service/RacingCarServiceTest.kt index 03de2c1c6c..68e6f3367a 100644 --- a/src/test/kotlin/step3/racingcar/service/RacingCarServiceTest.kt +++ b/src/test/kotlin/step3/racingcar/service/RacingCarServiceTest.kt @@ -1,67 +1,99 @@ package step3.racingcar.service import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe +import step3.racingcar.domain.Car import step3.racingcar.domain.Cars import step3.racingcar.domain.PlayInfo -import step3.racingcar.fixture.CarFixtureGenerator.Companion.난수를_가지는_차량_생성 +import step3.racingcar.fixture.TestRandomNumberGenerator internal class RacingCarServiceTest : BehaviorSpec({ - val racingCarService = RacingCarService() + lateinit var racingCarService: RacingCarService - given("참가 차량 한대에 숫자 4가 주어지고") { - val 참가_차량 = 난수를_가지는_차량_생성("참가 차량", 4) + given("단일 라운드를 진행할 참가 차량 한대가 주어지고") { + val 참가_차량 = Car("참가 차량") val given = Cars.of(listOf(참가_차량)) val playInfo = PlayInfo(given, 1) - `when`("해당 라운드를 진행하면") { + `when`("4이상의 난수가 발생하면") { + racingCarService = of(4) racingCarService.play(playInfo) - then("차량은 한칸 전진한다.") { + then("차량의 주행거리는 1 누적된다.") { 참가_차량.distance shouldBe 1 } } } - given("참가 차량 한대에 숫자 4와 3이 주어지고") { - val 참가_차량 = 난수를_가지는_차량_생성("참가 차량", 4, 3) + given("두 라운드를 진행할 참가 차량 한대가 주어지고") { + val 참가_차량 = Car("참가 차량") val given = Cars.of(listOf(참가_차량)) - val playInfo = PlayInfo(given, 2) - `when`("각 라운드를 진행하면") { - racingCarService.play(playInfo) - then("차량은 한칸 전진한다.") { + `when`("첫번째 라운드에 4이상의 난수가 발생하면") { + racingCarService = of(4) + racingCarService.playEachRound(given) + then("차량의 주행거리는 1 누적된다.") { + 참가_차량.distance shouldBe 1 + } + } + `when`("두번째 라운드에 3이하의 난수가 발생하면") { + racingCarService = of(3) + racingCarService.playEachRound(given) + then("차량의 주행거리는 누적되지 않는다.") { 참가_차량.distance shouldBe 1 } } } - given("두 차량에 각각 4와 3이 주어지고") { - val 첫번째_참가_차량 = 난수를_가지는_차량_생성("첫번째 참가 차량", 4) - val 두번째_참가_차량 = 난수를_가지는_차량_생성("두번째 참가 차량", 3) + given("단일 라운드를 진행할 참가 차량 두대가 주어지고") { + val 첫번째_참가_차량 = Car("첫번째_참가_차량") + val 두번째_참가_차량 = Car("두번째_참가_차량") val given = Cars.of(listOf(첫번째_참가_차량, 두번째_참가_차량)) - val playInfo = PlayInfo(given, 1) - `when`("해당 라운드를 진행하면") { - racingCarService.play(playInfo) - then("각 차량의 진행 거리를 확인할 수 있다.") { + `when`("첫번째 차량에는 4의 난수가, 두번째 차량에는 3의 난수가 발생하면") { + racingCarService = of(4).also { + it.playEachRoundByCar(첫번째_참가_차량) + } + racingCarService = of(3).also { + it.playEachRoundByCar(두번째_참가_차량) + } + then("첫번째 차량이 누적거리 1로 우승한다.") { 첫번째_참가_차량.distance shouldBe 1 - 두번째_참가_차량.distance shouldBe 0 + given.winnerNames().shouldContainExactly("첫번째_참가_차량") } } } - given("여러 차량에 숫자를 주입하고") { - val 첫번째_참가_차량 = 난수를_가지는_차량_생성("첫번째 참가 차량", 4, 5) - val 두번째_참가_차량 = 난수를_가지는_차량_생성("두번째 참가 차량", 3, 5) + given("두 라운드를 진행할 참가 차량 두대가 주어지고") { + val 첫번째_참가_차량 = Car("첫번째_참가_차량") + val 두번째_참가_차량 = Car("두번째_참가_차량") val given = Cars.of(listOf(첫번째_참가_차량, 두번째_참가_차량)) - val playInfo = PlayInfo(given, 2) - `when`("각 라운드를 진행하면") { - racingCarService.play(playInfo) - then("각 차량의 진행 거리를 확인할 수 있다.") { + `when`("첫번째 라운드에는 첫번째 차량에는 4의 난수가, 두번째 차량에는 3의 난수가 발생하고") { + racingCarService = of(4).also { + it.playEachRoundByCar(첫번째_참가_차량) + } + racingCarService = of(3).also { + it.playEachRoundByCar(두번째_참가_차량) + } + then("첫번째 차량을 우승 차량으로 반환한다.") { + given.winnerNames().shouldContainExactly("첫번째_참가_차량") + } + } + + `when`("두번째 라운드에는 두 차량 모두 4의 난수가 발생하면") { + racingCarService = of(4).also { + it.playEachRoundByCar(첫번째_참가_차량) + } + racingCarService = of(4).also { + it.playEachRoundByCar(두번째_참가_차량) + } + then("첫번째 차량이 누적거리 2로 우승한다.") { 첫번째_참가_차량.distance shouldBe 2 - 두번째_참가_차량.distance shouldBe 1 + given.winnerNames().shouldContainExactly("첫번째_참가_차량") } } } }) + +fun of(randomNumber: Int): RacingCarService = RacingCarService(TestRandomNumberGenerator(randomNumber))