diff --git a/README.md b/README.md index d0286c859f..16ac1abfe9 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ -# java-racingcar-precourse +# 자동차 경주 게임 + +## 기능 목록 + +### 1. 자동차 관리 +- [x] 자동차 이름 입력 및 검증 (5자 이하, 쉼표로 구분) +- [x] 자동차 위치 관리 및 전진 로직 + +### 2. 경주 진행 +- [x] 시도 횟수 입력 및 검증 +- [x] 각 라운드별 자동차 전진 여부 결정 (무작위 값 4 이상) +- [x] 경주 결과 출력 (각 라운드별 자동차 위치) + +### 3. 우승자 판정 +- [x] 최종 우승자 판정 (가장 많이 전진한 자동차) +- [x] 공동 우승자 처리 (여러 명일 경우 쉼표로 구분) + +### 4. 예외 처리 +- [x] 잘못된 입력값에 대한 IllegalArgumentException 발생 +- [x] 애플리케이션 종료 처리 + +## 실행 결과 예시 + +``` +경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) +pobi,woni,jun +시도할 횟수는 몇 회인가요? +5 + +실행 결과 +pobi : - +woni : +jun : - + +pobi : -- +woni : - +jun : -- + +pobi : --- +woni : -- +jun : --- + +pobi : ---- +woni : --- +jun : ---- + +pobi : ----- +woni : ---- +jun : ----- + +최종 우승자 : pobi, jun +``` diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..28dab7d101 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,33 @@ package racingcar; +import racingcar.domain.RacingGame; +import racingcar.view.InputView; +import racingcar.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + InputView inputView = new InputView(); + OutputView outputView = new OutputView(); + + RacingGame game = createRacingGame(inputView); + playRacingGame(game, outputView); + } + + private static RacingGame createRacingGame(InputView inputView) { + var cars = inputView.readCarNames(); + int rounds = inputView.readRounds(); + return new RacingGame(cars, rounds); + } + + private static void playRacingGame(RacingGame game, OutputView outputView) { + outputView.printResultHeader(); + + while (game.hasNextRound()) { + game.playNextRound(); + outputView.printRoundResult(game.getCars()); + } + + var result = game.getResult(); + outputView.printFinalResult(result); } } diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..7d11697641 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,71 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.Randoms; + +public class Car { + private static final int MOVING_THRESHOLD = 4; + private static final int MIN_RANDOM_VALUE = 0; + private static final int MAX_RANDOM_VALUE = 9; + + private final CarName name; + private int position; + + public Car(CarName name) { + this.name = name; + this.position = 0; + } + + public Car(CarName name, int position) { + this.name = name; + this.position = position; + } + + public void move() { + int randomValue = Randoms.pickNumberInRange(MIN_RANDOM_VALUE, MAX_RANDOM_VALUE); + if (randomValue >= MOVING_THRESHOLD) { + position++; + } + } + + public CarName getName() { + return name; + } + + public int getPosition() { + return position; + } + + public boolean isAtPosition(int targetPosition) { + return position == targetPosition; + } + + public String getPositionString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < position; i++) { + sb.append("-"); + } + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Car car = (Car) obj; + return position == car.position && name.equals(car.name); + } + + @Override + public int hashCode() { + return name.hashCode() + position; + } + + @Override + public String toString() { + return name.getName() + " : " + getPositionString(); + } +} diff --git a/src/main/java/racingcar/domain/CarName.java b/src/main/java/racingcar/domain/CarName.java new file mode 100644 index 0000000000..4846f0e2f0 --- /dev/null +++ b/src/main/java/racingcar/domain/CarName.java @@ -0,0 +1,49 @@ +package racingcar.domain; + +public class CarName { + private static final int MAX_NAME_LENGTH = 5; + private static final String EMPTY_NAME_ERROR = "자동차 이름은 비어있을 수 없습니다."; + private static final String INVALID_NAME_LENGTH_ERROR = "자동차 이름은 5자 이하여야 합니다."; + + private final String name; + + public CarName(String name) { + validateName(name); + this.name = name.trim(); + } + + private void validateName(String name) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException(EMPTY_NAME_ERROR); + } + if (name.trim().length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException(INVALID_NAME_LENGTH_ERROR); + } + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + CarName carName = (CarName) obj; + return name.equals(carName.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000000..b67951151d --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,77 @@ +package racingcar.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class Cars { + private static final String DUPLICATE_NAME_ERROR = "자동차 이름은 중복될 수 없습니다."; + + private final List cars; + + public Cars(List cars) { + validateCars(cars); + this.cars = new ArrayList<>(cars); + } + + private void validateCars(List cars) { + if (cars == null || cars.isEmpty()) { + throw new IllegalArgumentException("자동차 목록은 비어있을 수 없습니다."); + } + validateDuplicateNames(cars); + } + + private void validateDuplicateNames(List cars) { + List names = cars.stream() + .map(car -> car.getName().getName()) + .collect(Collectors.toList()); + + long uniqueNameCount = names.stream() + .distinct() + .count(); + + if (uniqueNameCount != names.size()) { + throw new IllegalArgumentException(DUPLICATE_NAME_ERROR); + } + } + + public void moveAll() { + for (Car car : cars) { + car.move(); + } + } + + public List getCars() { + return Collections.unmodifiableList(cars); + } + + public List getWinners() { + int maxPosition = getMaxPosition(); + return cars.stream() + .filter(car -> car.isAtPosition(maxPosition)) + .collect(Collectors.toList()); + } + + private int getMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } + + public int size() { + return cars.size(); + } + + public Car get(int index) { + return cars.get(index); + } + + @Override + public String toString() { + return cars.stream() + .map(Car::toString) + .collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/racingcar/domain/RaceResult.java b/src/main/java/racingcar/domain/RaceResult.java new file mode 100644 index 0000000000..18d8376e2e --- /dev/null +++ b/src/main/java/racingcar/domain/RaceResult.java @@ -0,0 +1,39 @@ +package racingcar.domain; + +import java.util.List; +import java.util.stream.Collectors; + +public class RaceResult { + private final List winners; + + public RaceResult(List winners) { + this.winners = winners; + } + + public List getWinners() { + return winners; + } + + public String getWinnerNames() { + return winners.stream() + .map(car -> car.getName().getName()) + .collect(Collectors.joining(", ")); + } + + public boolean hasSingleWinner() { + return winners.size() == 1; + } + + public boolean hasMultipleWinners() { + return winners.size() > 1; + } + + public int getWinnerCount() { + return winners.size(); + } + + @Override + public String toString() { + return "최종 우승자 : " + getWinnerNames(); + } +} diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java new file mode 100644 index 0000000000..ea43cc2504 --- /dev/null +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -0,0 +1,63 @@ +package racingcar.domain; + +import java.util.List; +import java.util.stream.Collectors; + +public class RacingGame { + private final Cars cars; + private final int totalRounds; + private int currentRound; + + public RacingGame(Cars cars, int totalRounds) { + validateTotalRounds(totalRounds); + this.cars = cars; + this.totalRounds = totalRounds; + this.currentRound = 0; + } + + private void validateTotalRounds(int totalRounds) { + if (totalRounds <= 0) { + throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); + } + } + + public boolean hasNextRound() { + return currentRound < totalRounds; + } + + public void playNextRound() { + if (!hasNextRound()) { + throw new IllegalStateException("더 이상 진행할 라운드가 없습니다."); + } + cars.moveAll(); + currentRound++; + } + + public RaceResult getResult() { + if (hasNextRound()) { + throw new IllegalStateException("경주가 아직 완료되지 않았습니다."); + } + List winners = cars.getWinners(); + return new RaceResult(winners); + } + + public Cars getCars() { + return cars; + } + + public int getCurrentRound() { + return currentRound; + } + + public int getTotalRounds() { + return totalRounds; + } + + public int getRemainingRounds() { + return totalRounds - currentRound; + } + + public boolean isFinished() { + return !hasNextRound(); + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..4e722d8be2 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,61 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; +import racingcar.domain.Car; +import racingcar.domain.CarName; +import racingcar.domain.Cars; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class InputView { + private static final String CAR_NAMES_PROMPT = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String ROUNDS_PROMPT = "시도할 횟수는 몇 회인가요?"; + private static final String INVALID_ROUNDS_ERROR = "시도 횟수는 숫자여야 합니다."; + private static final String INVALID_ROUNDS_RANGE_ERROR = "시도 횟수는 1 이상이어야 합니다."; + + public Cars readCarNames() { + System.out.println(CAR_NAMES_PROMPT); + String input = Console.readLine(); + return parseCarNames(input); + } + + private Cars parseCarNames(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException("자동차 이름을 입력해주세요."); + } + + List nameStrings = Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + + List cars = nameStrings.stream() + .map(name -> new Car(new CarName(name))) + .collect(Collectors.toList()); + + return new Cars(cars); + } + + public int readRounds() { + System.out.println(ROUNDS_PROMPT); + String input = Console.readLine(); + return parseRounds(input); + } + + private int parseRounds(String input) { + if (input == null || input.trim().isEmpty()) { + throw new IllegalArgumentException("시도 횟수를 입력해주세요."); + } + + try { + int rounds = Integer.parseInt(input.trim()); + if (rounds <= 0) { + throw new IllegalArgumentException(INVALID_ROUNDS_RANGE_ERROR); + } + return rounds; + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INVALID_ROUNDS_ERROR); + } + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..a704719a8c --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,22 @@ +package racingcar.view; + +import racingcar.domain.Cars; +import racingcar.domain.RaceResult; + +public class OutputView { + private static final String RESULT_HEADER = "실행 결과"; + + public void printResultHeader() { + System.out.println(); + System.out.println(RESULT_HEADER); + } + + public void printRoundResult(Cars cars) { + System.out.println(cars.toString()); + System.out.println(); + } + + public void printFinalResult(RaceResult result) { + System.out.println(result.toString()); + } +} diff --git a/src/test/java/racingcar/domain/CarNameTest.java b/src/test/java/racingcar/domain/CarNameTest.java new file mode 100644 index 0000000000..97dd353f32 --- /dev/null +++ b/src/test/java/racingcar/domain/CarNameTest.java @@ -0,0 +1,114 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CarNameTest { + + @Test + @DisplayName("정상적인 자동차 이름을 생성할 수 있다") + void createValidCarName() { + // given + String name = "pobi"; + + // when + CarName carName = new CarName(name); + + // then + assertThat(carName.getName()).isEqualTo("pobi"); + } + + @Test + @DisplayName("공백이 포함된 이름은 앞뒤 공백을 제거한다") + void trimWhitespace() { + // given + String name = " pobi "; + + // when + CarName carName = new CarName(name); + + // then + assertThat(carName.getName()).isEqualTo("pobi"); + } + + @Test + @DisplayName("5자 이름은 정상적으로 생성된다") + void createFiveCharacterName() { + // given + String name = "abcde"; + + // when + CarName carName = new CarName(name); + + // then + assertThat(carName.getName()).isEqualTo("abcde"); + } + + @ParameterizedTest + @ValueSource(strings = {"", " ", " ", " "}) + @DisplayName("빈 이름이나 공백만 있는 이름은 예외를 발생시킨다") + void throwExceptionForEmptyName(String invalidName) { + // when & then + assertThatThrownBy(() -> new CarName(invalidName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("null 이름은 예외를 발생시킨다") + void throwExceptionForNullName() { + // when & then + assertThatThrownBy(() -> new CarName(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("6자 이상의 이름은 예외를 발생시킨다") + void throwExceptionForLongName() { + // given + String longName = "abcdef"; + + // when & then + assertThatThrownBy(() -> new CarName(longName)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 5자 이하여야 합니다."); + } + + @Test + @DisplayName("같은 이름을 가진 CarName은 equals에서 true를 반환한다") + void equalsSameName() { + // given + CarName carName1 = new CarName("pobi"); + CarName carName2 = new CarName("pobi"); + + // when & then + assertThat(carName1).isEqualTo(carName2); + } + + @Test + @DisplayName("다른 이름을 가진 CarName은 equals에서 false를 반환한다") + void equalsDifferentName() { + // given + CarName carName1 = new CarName("pobi"); + CarName carName2 = new CarName("woni"); + + // when & then + assertThat(carName1).isNotEqualTo(carName2); + } + + @Test + @DisplayName("toString은 이름을 반환한다") + void toStringReturnsName() { + // given + CarName carName = new CarName("pobi"); + + // when & then + assertThat(carName.toString()).isEqualTo("pobi"); + } +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000000..d1d357e453 --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,127 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; +import static org.assertj.core.api.Assertions.assertThat; + +class CarTest extends NsTest { + + @Test + @DisplayName("자동차는 초기 위치가 0이다") + void carHasInitialPositionZero() { + // given + CarName name = new CarName("pobi"); + + // when + Car car = new Car(name); + + // then + assertThat(car.getPosition()).isEqualTo(0); + assertThat(car.getPositionString()).isEqualTo(""); + } + + @Test + @DisplayName("자동차는 무작위 값이 4 이상일 때 전진한다") + void carMovesWhenRandomValueIsFourOrMore() { + assertRandomNumberInRangeTest( + () -> { + // given + CarName name = new CarName("pobi"); + Car car = new Car(name); + + // when + car.move(); + + // then + assertThat(car.getPosition()).isEqualTo(1); + assertThat(car.getPositionString()).isEqualTo("-"); + }, + 4, 3 + ); + } + + @Test + @DisplayName("자동차는 무작위 값이 3 이하일 때 멈춘다") + void carStopsWhenRandomValueIsThreeOrLess() { + assertRandomNumberInRangeTest( + () -> { + // given + CarName name = new CarName("pobi"); + Car car = new Car(name); + + // when + car.move(); + + // then + assertThat(car.getPosition()).isEqualTo(0); + assertThat(car.getPositionString()).isEqualTo(""); + }, + 3, 4 + ); + } + + @Test + @DisplayName("자동차는 여러 번 전진할 수 있다") + void carCanMoveMultipleTimes() { + assertRandomNumberInRangeTest( + () -> { + // given + CarName name = new CarName("pobi"); + Car car = new Car(name); + + // when + car.move(); + car.move(); + car.move(); + + // then + assertThat(car.getPosition()).isEqualTo(3); + assertThat(car.getPositionString()).isEqualTo("---"); + }, + 4, 4, 4 + ); + } + + @Test + @DisplayName("자동차 이름을 반환한다") + void carReturnsName() { + // given + CarName name = new CarName("pobi"); + Car car = new Car(name); + + // when & then + assertThat(car.getName()).isEqualTo(name); + } + + @Test + @DisplayName("자동차는 특정 위치에 있는지 확인할 수 있다") + void carCanCheckPosition() { + // given + CarName name = new CarName("pobi"); + Car car = new Car(name, 3); + + // when & then + assertThat(car.isAtPosition(3)).isTrue(); + assertThat(car.isAtPosition(2)).isFalse(); + assertThat(car.isAtPosition(4)).isFalse(); + } + + @Test + @DisplayName("toString은 이름과 위치 문자열을 반환한다") + void toStringReturnsNameAndPosition() { + // given + CarName name = new CarName("pobi"); + Car car = new Car(name, 3); + + // when & then + assertThat(car.toString()).isEqualTo("pobi : ---"); + } + + @Override + public void runMain() { + // 테스트용 메인 메서드 + } +} diff --git a/src/test/java/racingcar/domain/CarsTest.java b/src/test/java/racingcar/domain/CarsTest.java new file mode 100644 index 0000000000..151742e325 --- /dev/null +++ b/src/test/java/racingcar/domain/CarsTest.java @@ -0,0 +1,169 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CarsTest extends NsTest { + + @Test + @DisplayName("자동차 목록을 생성할 수 있다") + void createCars() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("woni")) + ); + + // when + Cars cars = new Cars(carList); + + // then + assertThat(cars.size()).isEqualTo(2); + assertThat(cars.get(0).getName().getName()).isEqualTo("pobi"); + assertThat(cars.get(1).getName().getName()).isEqualTo("woni"); + } + + @Test + @DisplayName("빈 자동차 목록은 예외를 발생시킨다") + void throwExceptionForEmptyCars() { + // when & then + assertThatThrownBy(() -> new Cars(Arrays.asList())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 목록은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("null 자동차 목록은 예외를 발생시킨다") + void throwExceptionForNullCars() { + // when & then + assertThatThrownBy(() -> new Cars(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 목록은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("중복된 이름의 자동차는 예외를 발생시킨다") + void throwExceptionForDuplicateNames() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("pobi")) + ); + + // when & then + assertThatThrownBy(() -> new Cars(carList)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 중복될 수 없습니다."); + } + + @Test + @DisplayName("모든 자동차를 이동시킬 수 있다") + void moveAllCars() { + assertRandomNumberInRangeTest( + () -> { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("woni")) + ); + Cars cars = new Cars(carList); + + // when + cars.moveAll(); + + // then + assertThat(cars.get(0).getPosition()).isEqualTo(1); + assertThat(cars.get(1).getPosition()).isEqualTo(1); + }, + 4, 4 + ); + } + + @Test + @DisplayName("우승자를 찾을 수 있다") + void findWinners() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi"), 3), + new Car(new CarName("woni"), 2), + new Car(new CarName("jun"), 3) + ); + Cars cars = new Cars(carList); + + // when + List winners = cars.getWinners(); + + // then + assertThat(winners).hasSize(2); + assertThat(winners.get(0).getName().getName()).isEqualTo("pobi"); + assertThat(winners.get(1).getName().getName()).isEqualTo("jun"); + } + + @Test + @DisplayName("단독 우승자를 찾을 수 있다") + void findSingleWinner() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi"), 3), + new Car(new CarName("woni"), 2), + new Car(new CarName("jun"), 1) + ); + Cars cars = new Cars(carList); + + // when + List winners = cars.getWinners(); + + // then + assertThat(winners).hasSize(1); + assertThat(winners.get(0).getName().getName()).isEqualTo("pobi"); + } + + @Test + @DisplayName("모든 자동차가 같은 위치에 있으면 모두 우승자다") + void allCarsAreWinnersWhenSamePosition() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi"), 0), + new Car(new CarName("woni"), 0), + new Car(new CarName("jun"), 0) + ); + Cars cars = new Cars(carList); + + // when + List winners = cars.getWinners(); + + // then + assertThat(winners).hasSize(3); + } + + @Test + @DisplayName("toString은 모든 자동차의 상태를 반환한다") + void toStringReturnsAllCarsStatus() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi"), 2), + new Car(new CarName("woni"), 1) + ); + Cars cars = new Cars(carList); + + // when + String result = cars.toString(); + + // then + assertThat(result).contains("pobi : --"); + assertThat(result).contains("woni : -"); + } + + @Override + public void runMain() { + // 테스트용 메인 메서드 + } +} diff --git a/src/test/java/racingcar/domain/RaceResultTest.java b/src/test/java/racingcar/domain/RaceResultTest.java new file mode 100644 index 0000000000..1a9bda912e --- /dev/null +++ b/src/test/java/racingcar/domain/RaceResultTest.java @@ -0,0 +1,102 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RaceResultTest { + + @Test + @DisplayName("단독 우승자 결과를 생성할 수 있다") + void createSingleWinnerResult() { + // given + Car winner = new Car(new CarName("pobi"), 3); + List winners = Arrays.asList(winner); + + // when + RaceResult result = new RaceResult(winners); + + // then + assertThat(result.getWinners()).hasSize(1); + assertThat(result.getWinnerNames()).isEqualTo("pobi"); + assertThat(result.hasSingleWinner()).isTrue(); + assertThat(result.hasMultipleWinners()).isFalse(); + assertThat(result.getWinnerCount()).isEqualTo(1); + } + + @Test + @DisplayName("공동 우승자 결과를 생성할 수 있다") + void createMultipleWinnersResult() { + // given + List winners = Arrays.asList( + new Car(new CarName("pobi"), 3), + new Car(new CarName("jun"), 3) + ); + + // when + RaceResult result = new RaceResult(winners); + + // then + assertThat(result.getWinners()).hasSize(2); + assertThat(result.getWinnerNames()).isEqualTo("pobi, jun"); + assertThat(result.hasSingleWinner()).isFalse(); + assertThat(result.hasMultipleWinners()).isTrue(); + assertThat(result.getWinnerCount()).isEqualTo(2); + } + + @Test + @DisplayName("3명 이상의 공동 우승자 결과를 생성할 수 있다") + void createThreeWinnersResult() { + // given + List winners = Arrays.asList( + new Car(new CarName("pobi"), 3), + new Car(new CarName("woni"), 3), + new Car(new CarName("jun"), 3) + ); + + // when + RaceResult result = new RaceResult(winners); + + // then + assertThat(result.getWinners()).hasSize(3); + assertThat(result.getWinnerNames()).isEqualTo("pobi, woni, jun"); + assertThat(result.hasSingleWinner()).isFalse(); + assertThat(result.hasMultipleWinners()).isTrue(); + assertThat(result.getWinnerCount()).isEqualTo(3); + } + + @Test + @DisplayName("toString은 우승자 정보를 반환한다") + void toStringReturnsWinnerInfo() { + // given + List winners = Arrays.asList( + new Car(new CarName("pobi"), 3), + new Car(new CarName("jun"), 3) + ); + RaceResult result = new RaceResult(winners); + + // when + String resultString = result.toString(); + + // then + assertThat(resultString).isEqualTo("최종 우승자 : pobi, jun"); + } + + @Test + @DisplayName("단독 우승자의 toString은 한 명의 이름만 반환한다") + void toStringForSingleWinner() { + // given + List winners = Arrays.asList(new Car(new CarName("pobi"), 3)); + RaceResult result = new RaceResult(winners); + + // when + String resultString = result.toString(); + + // then + assertThat(resultString).isEqualTo("최종 우승자 : pobi"); + } +} diff --git a/src/test/java/racingcar/domain/RacingGameTest.java b/src/test/java/racingcar/domain/RacingGameTest.java new file mode 100644 index 0000000000..e47719cc08 --- /dev/null +++ b/src/test/java/racingcar/domain/RacingGameTest.java @@ -0,0 +1,168 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RacingGameTest extends NsTest { + + @Test + @DisplayName("경주 게임을 생성할 수 있다") + void createRacingGame() { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("woni")) + ); + Cars cars = new Cars(carList); + int totalRounds = 3; + + // when + RacingGame game = new RacingGame(cars, totalRounds); + + // then + assertThat(game.getTotalRounds()).isEqualTo(3); + assertThat(game.getCurrentRound()).isEqualTo(0); + assertThat(game.getRemainingRounds()).isEqualTo(3); + assertThat(game.hasNextRound()).isTrue(); + assertThat(game.isFinished()).isFalse(); + } + + @Test + @DisplayName("시도 횟수가 0 이하면 예외를 발생시킨다") + void throwExceptionForInvalidRounds() { + // given + List carList = Arrays.asList(new Car(new CarName("pobi"))); + Cars cars = new Cars(carList); + + // when & then + assertThatThrownBy(() -> new RacingGame(cars, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도 횟수는 1 이상이어야 합니다."); + + assertThatThrownBy(() -> new RacingGame(cars, -1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도 횟수는 1 이상이어야 합니다."); + } + + @Test + @DisplayName("다음 라운드를 진행할 수 있다") + void playNextRound() { + assertRandomNumberInRangeTest( + () -> { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("woni")) + ); + Cars cars = new Cars(carList); + RacingGame game = new RacingGame(cars, 2); + + // when + game.playNextRound(); + + // then + assertThat(game.getCurrentRound()).isEqualTo(1); + assertThat(game.getRemainingRounds()).isEqualTo(1); + assertThat(game.hasNextRound()).isTrue(); + assertThat(game.isFinished()).isFalse(); + }, + 4, 4 + ); + } + + @Test + @DisplayName("모든 라운드를 완료할 수 있다") + void completeAllRounds() { + assertRandomNumberInRangeTest( + () -> { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("woni")) + ); + Cars cars = new Cars(carList); + RacingGame game = new RacingGame(cars, 2); + + // when + game.playNextRound(); + game.playNextRound(); + + // then + assertThat(game.getCurrentRound()).isEqualTo(2); + assertThat(game.getRemainingRounds()).isEqualTo(0); + assertThat(game.hasNextRound()).isFalse(); + assertThat(game.isFinished()).isTrue(); + }, + 4, 4, 4, 4 + ); + } + + @Test + @DisplayName("완료되지 않은 게임에서 결과를 가져오면 예외를 발생시킨다") + void throwExceptionWhenGettingResultBeforeCompletion() { + // given + List carList = Arrays.asList(new Car(new CarName("pobi"))); + Cars cars = new Cars(carList); + RacingGame game = new RacingGame(cars, 2); + + // when & then + assertThatThrownBy(() -> game.getResult()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("경주가 아직 완료되지 않았습니다."); + } + + @Test + @DisplayName("완료된 게임에서 결과를 가져올 수 있다") + void getResultAfterCompletion() { + assertRandomNumberInRangeTest( + () -> { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi")), + new Car(new CarName("woni")) + ); + Cars cars = new Cars(carList); + RacingGame game = new RacingGame(cars, 1); + + // when + game.playNextRound(); + RaceResult result = game.getResult(); + + // then + assertThat(result).isNotNull(); + assertThat(result.getWinners()).isNotEmpty(); + }, + 4, 4 + ); + } + + @Test + @DisplayName("더 이상 진행할 라운드가 없을 때 라운드를 진행하면 예외를 발생시킨다") + void throwExceptionWhenPlayingRoundAfterCompletion() { + // given + List carList = Arrays.asList(new Car(new CarName("pobi"))); + Cars cars = new Cars(carList); + RacingGame game = new RacingGame(cars, 1); + + // when + game.playNextRound(); + + // then + assertThatThrownBy(() -> game.playNextRound()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("더 이상 진행할 라운드가 없습니다."); + } + + @Override + public void runMain() { + // 테스트용 메인 메서드 + } +} diff --git a/src/test/java/racingcar/view/InputViewTest.java b/src/test/java/racingcar/view/InputViewTest.java new file mode 100644 index 0000000000..b31cebcdff --- /dev/null +++ b/src/test/java/racingcar/view/InputViewTest.java @@ -0,0 +1,135 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Cars; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class InputViewTest extends NsTest { + + @Test + @DisplayName("정상적인 자동차 이름을 입력받을 수 있다") + void readValidCarNames() { + assertSimpleTest(() -> { + // given + run("pobi,woni,jun"); + + // when + InputView inputView = new InputView(); + Cars cars = inputView.readCarNames(); + + // then + assertThat(cars.size()).isEqualTo(3); + assertThat(cars.get(0).getName().getName()).isEqualTo("pobi"); + assertThat(cars.get(1).getName().getName()).isEqualTo("woni"); + assertThat(cars.get(2).getName().getName()).isEqualTo("jun"); + }); + } + + @Test + @DisplayName("공백이 포함된 자동차 이름을 처리할 수 있다") + void readCarNamesWithWhitespace() { + assertSimpleTest(() -> { + // given + run(" pobi , woni , jun "); + + // when + InputView inputView = new InputView(); + Cars cars = inputView.readCarNames(); + + // then + assertThat(cars.size()).isEqualTo(3); + assertThat(cars.get(0).getName().getName()).isEqualTo("pobi"); + assertThat(cars.get(1).getName().getName()).isEqualTo("woni"); + assertThat(cars.get(2).getName().getName()).isEqualTo("jun"); + }); + } + + + @Test + @DisplayName("중복된 자동차 이름은 예외를 발생시킨다") + void throwExceptionForDuplicateCarNames() { + assertSimpleTest(() -> { + // given + run("pobi,pobi"); + + // when & then + InputView inputView = new InputView(); + assertThatThrownBy(() -> inputView.readCarNames()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 중복될 수 없습니다."); + }); + } + + @Test + @DisplayName("정상적인 시도 횟수를 입력받을 수 있다") + void readValidRounds() { + assertSimpleTest(() -> { + // given + run("5"); + + // when + InputView inputView = new InputView(); + int rounds = inputView.readRounds(); + + // then + assertThat(rounds).isEqualTo(5); + }); + } + + @Test + @DisplayName("공백이 포함된 시도 횟수를 처리할 수 있다") + void readRoundsWithWhitespace() { + assertSimpleTest(() -> { + // given + run(" 3 "); + + // when + InputView inputView = new InputView(); + int rounds = inputView.readRounds(); + + // then + assertThat(rounds).isEqualTo(3); + }); + } + + + @Test + @DisplayName("숫자가 아닌 시도 횟수는 예외를 발생시킨다") + void throwExceptionForNonNumericRounds() { + assertSimpleTest(() -> { + // given + run("abc"); + + // when & then + InputView inputView = new InputView(); + assertThatThrownBy(() -> inputView.readRounds()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도 횟수는 숫자여야 합니다."); + }); + } + + @Test + @DisplayName("0 이하의 시도 횟수는 예외를 발생시킨다") + void throwExceptionForInvalidRounds() { + assertSimpleTest(() -> { + // given + run("0"); + + // when & then + InputView inputView = new InputView(); + assertThatThrownBy(() -> inputView.readRounds()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시도 횟수는 1 이상이어야 합니다."); + }); + } + + @Override + public void runMain() { + // 테스트용 메인 메서드 + } +} diff --git a/src/test/java/racingcar/view/OutputViewTest.java b/src/test/java/racingcar/view/OutputViewTest.java new file mode 100644 index 0000000000..42eaaecfba --- /dev/null +++ b/src/test/java/racingcar/view/OutputViewTest.java @@ -0,0 +1,96 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.domain.CarName; +import racingcar.domain.Cars; +import racingcar.domain.RaceResult; + +import java.util.Arrays; +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; + +class OutputViewTest extends NsTest { + + @Test + @DisplayName("결과 헤더를 출력할 수 있다") + void printResultHeader() { + assertSimpleTest(() -> { + // given + OutputView outputView = new OutputView(); + + // when + outputView.printResultHeader(); + + // then + assertThat(output()).contains("실행 결과"); + }); + } + + @Test + @DisplayName("라운드 결과를 출력할 수 있다") + void printRoundResult() { + assertSimpleTest(() -> { + // given + List carList = Arrays.asList( + new Car(new CarName("pobi"), 2), + new Car(new CarName("woni"), 1) + ); + Cars cars = new Cars(carList); + OutputView outputView = new OutputView(); + + // when + outputView.printRoundResult(cars); + + // then + assertThat(output()).contains("pobi : --"); + assertThat(output()).contains("woni : -"); + }); + } + + @Test + @DisplayName("최종 결과를 출력할 수 있다") + void printFinalResult() { + assertSimpleTest(() -> { + // given + List winners = Arrays.asList( + new Car(new CarName("pobi"), 3), + new Car(new CarName("jun"), 3) + ); + RaceResult result = new RaceResult(winners); + OutputView outputView = new OutputView(); + + // when + outputView.printFinalResult(result); + + // then + assertThat(output()).contains("최종 우승자 : pobi, jun"); + }); + } + + @Test + @DisplayName("단독 우승자 결과를 출력할 수 있다") + void printSingleWinnerResult() { + assertSimpleTest(() -> { + // given + List winners = Arrays.asList(new Car(new CarName("pobi"), 3)); + RaceResult result = new RaceResult(winners); + OutputView outputView = new OutputView(); + + // when + outputView.printFinalResult(result); + + // then + assertThat(output()).contains("최종 우승자 : pobi"); + }); + } + + @Override + public void runMain() { + // 테스트용 메인 메서드 + } +}