diff --git a/README.md b/README.md index d0286c859f..78f9b35a0a 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# java-racingcar-precourse +## 기능 구현 목록 + +### 1. 입력 기능 +- [x] 자동차 이름 입력 받기 +- [x] 시도 횟수 입력 받기 + +### 2. 입력 검증 기능 +**자동차 이름 검증** +- [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리 +- [x] 자동차 이름이 5자를 초과하는 경우 예외 처리 +- [x] 자동차 이름이 빈 문자열인 경우 예외 처리 +- [x] 자동차 이름이 공백만 있는 경우 예외 처리 + +**시도 횟수 검증** +- [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리 +- [x] 숫자가 아닌 값 입력 시 예외 처리 +- [x] 0 이하의 값 입력 시 예외 처리 +- [x] 정수 범위를 벗어나는 값 입력 시 예외 처리 + +### 3. 입력 파싱 기능 +- [x] 쉼표(,)를 기준으로 자동차 이름 분리 +- [x] 입력받은 시도 횟수를 정수로 변환 + +### 4. 자동차 기능 +- [x] 자동차 객체 생성 (이름 저장) +- [x] 자동차의 현재 위치 저장 +- [x] 자동차 전진 기능 +- [x] 자동차의 현재 위치 조회 + +### 5. 게임 진행 기능 +- [x] 게임 라운드 반복 실행 (n회) +- [x] 각 라운드마다 모든 자동차에 대해 이동 시도 +- [x] 0~9 사이의 무작위 값 생성 +- [x] 무작위 값이 4 이상일 경우 자동차 전진 + +### 6. 우승자 판별 기능 +- [x] 모든 자동차 중 최대 이동 거리 찾기 +- [x] 최대 이동 거리를 가진 자동차들을 우승자로 선정 + +### 7. 출력 기능 +- [x] 각 라운드 후 모든 자동차의 이름과 위치 출력 + - [x] 자동차 위치를 `-` 기호로 표시 +- [x] 우승자 이름 출력 (단독 우승자) +- [x] 우승자 이름 출력 (공동 우승자, 쉼표로 구분) + +--- + diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..dd6bf24561 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,10 @@ package racingcar; +import racingcar.controller.RacingGameController; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + RacingGameController controller = new RacingGameController(); + controller.run(); } } diff --git a/src/main/java/racingcar/controller/RacingGameController.java b/src/main/java/racingcar/controller/RacingGameController.java new file mode 100644 index 0000000000..e1592cd3a1 --- /dev/null +++ b/src/main/java/racingcar/controller/RacingGameController.java @@ -0,0 +1,53 @@ +package racingcar.controller; + +import racingcar.domain.Car; +import racingcar.domain.RacingGame; +import racingcar.validator.InputValidator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.List; + +public class RacingGameController { + + public void run() { + List carNames = readAndValidateCarNames(); + int roundCount = readAndValidateRoundCount(); + + RacingGame game = new RacingGame(carNames, roundCount); + + OutputView.printResultMessage(); + playGameWithOutput(game, roundCount); + + List winners = game.getWinners(); + OutputView.printWinners(winners); + } + + private List readAndValidateCarNames() { + List carNames = InputView.readCarNames(); + String input = String.join(",", carNames); + InputValidator.validateCarNames(input); + return carNames; + } + + private int readAndValidateRoundCount() { + int roundCount = InputView.readRoundCount(); + InputValidator.validateRoundCount(String.valueOf(roundCount)); + return roundCount; + } + + private void playGameWithOutput(RacingGame game, int roundCount) { + for (int i = 0; i < roundCount; i++) { + game.playRound(); + printRoundResult(game); + } + } + + private void printRoundResult(RacingGame game) { + List cars = game.getCars(); + for (Car car : cars) { + OutputView.printCarStatus(car.getName(), car.getPosition()); + } + OutputView.printRoundResult(); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..71d56917f0 --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,39 @@ +package racingcar.domain; + +public class Car { + private static final int MAX_NAME_LENGTH = 5; + private static final int INITIAL_POSITION = 0; + + private final String name; + private int position; + + public Car(String name) { + validateName(name); + this.name = name; + this.position = INITIAL_POSITION; + } + + private void validateName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다."); + } + if (name.isBlank()) { + throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); + } + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public void moveForward() { + position++; + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java new file mode 100644 index 0000000000..79cfbde435 --- /dev/null +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -0,0 +1,61 @@ +package racingcar.domain; + +import camp.nextstep.edu.missionutils.Randoms; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class RacingGame { + private static final int RANDOM_NUMBER_MIN = 0; + private static final int RANDOM_NUMBER_MAX = 9; + private static final int MOVE_FORWARD_THRESHOLD = 4; + + private final List cars; + private final int rounds; + + public RacingGame(List carNames, int rounds) { + this.cars = carNames.stream() + .map(Car::new) + .collect(Collectors.toList()); + this.rounds = rounds; + } + + public List getCars() { + return new ArrayList<>(cars); + } + + public void play() { + for (int i = 0; i < rounds; i++) { + playRound(); + } + } + + public void playRound() { + for (Car car : cars) { + tryMove(car); + } + } + + private void tryMove(Car car) { + int randomNumber = Randoms.pickNumberInRange(RANDOM_NUMBER_MIN, RANDOM_NUMBER_MAX); + if (randomNumber >= MOVE_FORWARD_THRESHOLD) { + car.moveForward(); + } + } + + public List getWinners() { + int maxPosition = findMaxPosition(); + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .map(Car::getName) + .collect(Collectors.toList()); + } + + private int findMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/validator/InputValidator.java b/src/main/java/racingcar/validator/InputValidator.java new file mode 100644 index 0000000000..c3185104c0 --- /dev/null +++ b/src/main/java/racingcar/validator/InputValidator.java @@ -0,0 +1,52 @@ +package racingcar.validator; + +public class InputValidator { + private static final int MAX_NAME_LENGTH = 5; + + private InputValidator() { + } + + public static void validateCarNames(String carNames) { + if (carNames == null || carNames.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다."); + } + + String[] names = carNames.split(",", -1); + for (String name : names) { + validateSingleCarName(name.trim()); + } + } + + private static void validateSingleCarName(String name) { + if (name.isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 빈 문자열일 수 없습니다."); + } + if (name.isBlank()) { + throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다."); + } + if (name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); + } + } + + public static void validateRoundCount(String roundCount) { + if (roundCount == null || roundCount.isEmpty()) { + throw new IllegalArgumentException("시도 횟수는 null이거나 빈 문자열일 수 없습니다."); + } + + if (roundCount.isBlank()) { + throw new IllegalArgumentException("시도 횟수는 공백일 수 없습니다."); + } + + int count; + try { + count = Integer.parseInt(roundCount); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("시도 횟수는 숫자여야 합니다."); + } + + if (count <= 0) { + throw new IllegalArgumentException("시도 횟수는 0보다 커야 합니다."); + } + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..c481ca51c8 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,37 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class InputView { + private static final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String ROUND_COUNT_INPUT_MESSAGE = "시도할 횟수는 몇 회인가요?"; + + private InputView() { + } + + public static List readCarNames() { + System.out.println(CAR_NAMES_INPUT_MESSAGE); + String input = Console.readLine(); + return parseCarNames(input); + } + + public static List parseCarNames(String input) { + return Arrays.stream(input.split(",")) + .map(String::trim) + .collect(Collectors.toList()); + } + + public static int readRoundCount() { + System.out.println(ROUND_COUNT_INPUT_MESSAGE); + String input = Console.readLine(); + return parseRoundCount(input); + } + + public static int parseRoundCount(String input) { + return Integer.parseInt(input); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..19688f57b2 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,40 @@ +package racingcar.view; + +import java.util.List; + +public class OutputView { + private static final String RESULT_MESSAGE = "실행 결과"; + private static final String WINNER_PREFIX = "최종 우승자 : "; + private static final String POSITION_SYMBOL = "-"; + private static final String CAR_STATUS_FORMAT = "%s : %s"; + private static final String WINNER_DELIMITER = ", "; + + private OutputView() { + } + + public static void printResultMessage() { + System.out.println(); + System.out.println(RESULT_MESSAGE); + } + + public static void printCarStatus(String carName, int position) { + System.out.println(formatCarStatus(carName, position)); + } + + public static String formatCarStatus(String carName, int position) { + String positionDisplay = POSITION_SYMBOL.repeat(position); + return String.format(CAR_STATUS_FORMAT, carName, positionDisplay); + } + + public static void printRoundResult() { + System.out.println(); + } + + public static void printWinners(List winners) { + System.out.println(formatWinners(winners)); + } + + public static String formatWinners(List winners) { + return WINNER_PREFIX + String.join(WINNER_DELIMITER, winners); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java index 1d35fc33fe..df596a5256 100644 --- a/src/test/java/racingcar/ApplicationTest.java +++ b/src/test/java/racingcar/ApplicationTest.java @@ -1,6 +1,7 @@ package racingcar; 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; @@ -13,6 +14,7 @@ class ApplicationTest extends NsTest { private static final int STOP = 3; @Test + @DisplayName("단독 우승자 기능 테스트") void 기능_테스트() { assertRandomNumberInRangeTest( () -> { @@ -24,6 +26,32 @@ class ApplicationTest extends NsTest { } @Test + @DisplayName("공동 우승자 기능 테스트") + void 공동_우승자_테스트() { + assertRandomNumberInRangeTest( + () -> { + run("pobi,woni,jun", "1"); + assertThat(output()).contains("pobi : -", "woni : -", "jun : ", "최종 우승자 : pobi, woni"); + }, + MOVING_FORWARD, MOVING_FORWARD, STOP + ); + } + + @Test + @DisplayName("여러 라운드 실행 테스트") + void 여러_라운드_테스트() { + assertRandomNumberInRangeTest( + () -> { + run("pobi,woni", "5"); + assertThat(output()).contains("pobi :", "woni :", "최종 우승자"); + }, + MOVING_FORWARD, STOP, MOVING_FORWARD, STOP, MOVING_FORWARD, STOP, + MOVING_FORWARD, STOP, MOVING_FORWARD, STOP + ); + } + + @Test + @DisplayName("자동차 이름이 5자를 초과하면 예외 발생") void 예외_테스트() { assertSimpleTest(() -> assertThatThrownBy(() -> runException("pobi,javaji", "1")) @@ -31,6 +59,63 @@ class ApplicationTest extends NsTest { ); } + @Test + @DisplayName("자동차 이름이 빈 문자열이면 예외 발생") + void 빈_이름_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,,woni", "1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("시도 횟수가 0이면 예외 발생") + void 시도_횟수_0_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "0")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("시도 횟수가 음수면 예외 발생") + void 시도_횟수_음수_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "-1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("시도 횟수가 숫자가 아니면 예외 발생") + void 시도_횟수_문자_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,woni", "abc")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("자동차 이름이 null이면 예외 발생") + void 자동차_이름_null_예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("", "5")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + @DisplayName("단일 자동차로 게임 실행") + void 단일_자동차_테스트() { + assertRandomNumberInRangeTest( + () -> { + run("pobi", "3"); + assertThat(output()).contains("pobi :", "최종 우승자 : pobi"); + }, + MOVING_FORWARD, MOVING_FORWARD, MOVING_FORWARD + ); + } + @Override public void runMain() { Application.main(new String[]{}); diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000000..c757da6feb --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,128 @@ +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.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CarTest { + + @Test + @DisplayName("자동차 생성 시 이름이 정상적으로 저장된다") + void createCarWithName() { + // given + String name = "pobi"; + + // when + Car car = new Car(name); + + // then + assertThat(car.getName()).isEqualTo(name); + } + + @Test + @DisplayName("자동차 생성 시 초기 위치는 0이다") + void createCarWithInitialPosition() { + // given + String name = "pobi"; + + // when + Car car = new Car(name); + + // then + assertThat(car.getPosition()).isEqualTo(0); + } + + @Test + @DisplayName("자동차가 전진하면 위치가 1 증가한다") + void moveForward() { + // given + Car car = new Car("pobi"); + int initialPosition = car.getPosition(); + + // when + car.moveForward(); + + // then + assertThat(car.getPosition()).isEqualTo(initialPosition + 1); + } + + @Test + @DisplayName("자동차가 여러 번 전진하면 위치가 누적된다") + void moveForwardMultipleTimes() { + // given + Car car = new Car("pobi"); + + // when + car.moveForward(); + car.moveForward(); + car.moveForward(); + + // then + assertThat(car.getPosition()).isEqualTo(3); + } + + @Test + @DisplayName("자동차 이름이 5자를 초과하면 예외가 발생한다") + void createCarWithNameLongerThan5() { + // given + String name = "pobi12"; + + // when & then + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("자동차 이름이 null이거나 빈 문자열이면 예외가 발생한다") + void createCarWithNullOrEmptyName(String name) { + // when & then + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @ValueSource(strings = {" ", " ", " "}) + @DisplayName("자동차 이름이 공백만 있으면 예외가 발생한다") + void createCarWithBlankName(String name) { + // when & then + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @Test + @DisplayName("다른 자동차와 위치를 비교할 수 있다") + void comparePosition() { + // given + Car car1 = new Car("pobi"); + Car car2 = new Car("woni"); + + car1.moveForward(); + car1.moveForward(); + car2.moveForward(); + + // when & then + assertThat(car1.getPosition()).isGreaterThan(car2.getPosition()); + } + + @Test + @DisplayName("자동차 이름은 5자까지 가능하다") + void createCarWithName5Characters() { + // given + String name = "pobi1"; + + // when + Car car = new Car(name); + + // then + assertThat(car.getName()).isEqualTo(name); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/RacingGameTest.java b/src/test/java/racingcar/domain/RacingGameTest.java new file mode 100644 index 0000000000..bbcb955c01 --- /dev/null +++ b/src/test/java/racingcar/domain/RacingGameTest.java @@ -0,0 +1,163 @@ +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 RacingGameTest { + + @Test + @DisplayName("게임 생성 시 자동차들이 정상적으로 저장된다") + void createGameWithCars() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + + // when + RacingGame game = new RacingGame(carNames, 5); + + // then + assertThat(game.getCars()).hasSize(3); + } + + @Test + @DisplayName("라운드 실행 시 모든 자동차가 이동을 시도한다") + void playOneRound() { + // given + List carNames = Arrays.asList("pobi", "woni"); + RacingGame game = new RacingGame(carNames, 1); + + // when + game.playRound(); + + // then + List cars = game.getCars(); + assertThat(cars).allMatch(car -> car.getPosition() >= 0); + } + + @Test + @DisplayName("여러 라운드를 실행할 수 있다") + void playMultipleRounds() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + int rounds = 5; + RacingGame game = new RacingGame(carNames, rounds); + + // when + game.play(); + + // then + List cars = game.getCars(); + assertThat(cars).allMatch(car -> car.getPosition() >= 0); + } + + @Test + @DisplayName("우승자를 판별할 수 있다 - 단독 우승") + void findSingleWinner() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames, 5); + + // 특정 자동차만 전진시키기 (테스트를 위해 직접 조작) + List cars = game.getCars(); + cars.get(0).moveForward(); // pobi 전진 + cars.get(0).moveForward(); // pobi 전진 + + // when + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactly("pobi"); + } + + @Test + @DisplayName("우승자를 판별할 수 있다 - 공동 우승") + void findMultipleWinners() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames, 5); + + // 두 자동차를 같은 거리로 전진시키기 + List cars = game.getCars(); + cars.get(0).moveForward(); // pobi + cars.get(0).moveForward(); // pobi + cars.get(1).moveForward(); // woni + cars.get(1).moveForward(); // woni + cars.get(2).moveForward(); // jun + + // when + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactlyInAnyOrder("pobi", "woni"); + } + + @Test + @DisplayName("모든 자동차가 같은 위치면 모두 우승자이다") + void findAllWinnersWhenTied() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + RacingGame game = new RacingGame(carNames, 5); + + // 모든 자동차를 같은 거리로 + List cars = game.getCars(); + cars.forEach(car -> { + car.moveForward(); + car.moveForward(); + }); + + // when + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactlyInAnyOrder("pobi", "woni", "jun"); + } + + @Test + @DisplayName("한 대의 자동차만 있어도 게임을 진행할 수 있다") + void playGameWithSingleCar() { + // given + List carNames = List.of("pobi"); + RacingGame game = new RacingGame(carNames, 1); + + // when + game.playRound(); + List winners = game.getWinners(); + + // then + assertThat(winners).containsExactly("pobi"); + } + + @Test + @DisplayName("게임 시작 시 모든 자동차의 위치는 0이다") + void initialPositionIsZero() { + // given + List carNames = Arrays.asList("pobi", "woni", "jun"); + + // when + RacingGame game = new RacingGame(carNames, 5); + + // then + List cars = game.getCars(); + assertThat(cars).allMatch(car -> car.getPosition() == 0); + } + + @Test + @DisplayName("현재 자동차 목록을 조회할 수 있다") + void getCars() { + // given + List carNames = Arrays.asList("pobi", "woni"); + RacingGame game = new RacingGame(carNames, 5); + + // when + List cars = game.getCars(); + + // then + assertThat(cars).hasSize(2); + assertThat(cars.get(0).getName()).isEqualTo("pobi"); + assertThat(cars.get(1).getName()).isEqualTo("woni"); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/validator/InputValidatorTest.java b/src/test/java/racingcar/validator/InputValidatorTest.java new file mode 100644 index 0000000000..c6f5b9983f --- /dev/null +++ b/src/test/java/racingcar/validator/InputValidatorTest.java @@ -0,0 +1,141 @@ +package racingcar.validator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class InputValidatorTest { + + @Test + @DisplayName("정상적인 자동차 이름들을 검증한다") + void validateValidCarNames() { + // given + String carNames = "pobi,woni,jun"; + + // when & then + assertThatCode(() -> InputValidator.validateCarNames(carNames)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("자동차 이름 입력이 null이거나 빈 문자열이면 예외가 발생한다") + void validateNullOrEmptyCarNames(String carNames) { + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @Test + @DisplayName("자동차 이름 중 5자를 초과하는 이름이 있으면 예외가 발생한다") + void validateCarNameLongerThan5() { + // given + String carNames = "pobi,javaji,woni"; + + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @ValueSource(strings = {"pobi,,woni", ",pobi,woni", "pobi,woni,", "pobi, ,woni"}) + @DisplayName("자동차 이름 중 빈 이름이 있으면 예외가 발생한다") + void validateCarNamesWithEmpty(String carNames) { + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @ParameterizedTest + @ValueSource(strings = {"pobi, ,woni", "pobi, ,jun"}) + @DisplayName("자동차 이름 중 공백만 있는 이름이 있으면 예외가 발생한다") + void validateCarNamesWithBlank(String carNames) { + // when & then + assertThatThrownBy(() -> InputValidator.validateCarNames(carNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("이름"); + } + + @Test + @DisplayName("자동차 이름이 하나만 있어도 정상적으로 검증된다") + void validateSingleCarName() { + // given + String carNames = "pobi"; + + // when & then + assertThatCode(() -> InputValidator.validateCarNames(carNames)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("정상적인 시도 횟수를 검증한다") + void validateValidRoundCount() { + // given + String roundCount = "5"; + + // when & then + assertThatCode(() -> InputValidator.validateRoundCount(roundCount)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + @DisplayName("시도 횟수가 null이거나 빈 문자열이면 예외가 발생한다") + void validateNullOrEmptyRoundCount(String roundCount) { + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @ParameterizedTest + @ValueSource(strings = {"abc", "1.5", "일", "1a", " "}) + @DisplayName("시도 횟수가 숫자가 아니면 예외가 발생한다") + void validateNonNumericRoundCount(String roundCount) { + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @ParameterizedTest + @ValueSource(strings = {"0", "-1", "-100"}) + @DisplayName("시도 횟수가 0 이하이면 예외가 발생한다") + void validateRoundCountLessThanOrEqualToZero(String roundCount) { + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @Test + @DisplayName("시도 횟수가 정수 범위를 벗어나면 예외가 발생한다") + void validateRoundCountOutOfIntegerRange() { + // given + String roundCount = "9999999999999999999"; + + // when & then + assertThatThrownBy(() -> InputValidator.validateRoundCount(roundCount)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시도 횟수"); + } + + @Test + @DisplayName("시도 횟수가 10이면 정상적으로 검증된다") + void validateRoundCountOne() { + // given + String roundCount = "10"; + + // when & then + assertThatCode(() -> InputValidator.validateRoundCount(roundCount)) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/view/InputViewTest.java b/src/test/java/racingcar/view/InputViewTest.java new file mode 100644 index 0000000000..343f633cf1 --- /dev/null +++ b/src/test/java/racingcar/view/InputViewTest.java @@ -0,0 +1,97 @@ +package racingcar.view; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class InputViewTest { + + @Test + @DisplayName("쉼표로 구분된 자동차 이름을 파싱한다") + void parseCarNames() { + // given + String input = "pobi,woni,jun"; + + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).containsExactly("pobi", "woni", "jun"); + } + + @Test + @DisplayName("자동차 이름이 하나만 있어도 파싱된다") + void parseSingleCarName() { + // given + String input = "pobi"; + + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).containsExactly("pobi"); + } + + @ParameterizedTest + @CsvSource({ + "'pobi,woni,jun', 3", + "'pobi,woni', 2", + "'pobi', 1", + "'a,b,c,d,e', 5" + }) + @DisplayName("여러 개의 자동차 이름을 정확히 파싱한다") + void parseMultipleCarNames(String input, int expectedSize) { + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).hasSize(expectedSize); + } + + @Test + @DisplayName("시도 횟수 문자열을 정수로 변환한다") + void parseRoundCount() { + // given + String input = "5"; + + // when + int roundCount = InputView.parseRoundCount(input); + + // then + assertThat(roundCount).isEqualTo(5); + } + + @ParameterizedTest + @CsvSource({ + "1, 1", + "5, 5", + "10, 10", + "100, 100" + }) + @DisplayName("다양한 시도 횟수를 정확히 파싱한다") + void parseVariousRoundCounts(String input, int expected) { + // when + int roundCount = InputView.parseRoundCount(input); + + // then + assertThat(roundCount).isEqualTo(expected); + } + + @Test + @DisplayName("자동차 이름 사이의 공백은 제거된다") + void trimSpacesInCarNames() { + // given + String input = "pobi, woni, jun"; + + // when + List carNames = InputView.parseCarNames(input); + + // then + assertThat(carNames).containsExactly("pobi", "woni", "jun"); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/view/OutputViewTest.java b/src/test/java/racingcar/view/OutputViewTest.java new file mode 100644 index 0000000000..d47f51f673 --- /dev/null +++ b/src/test/java/racingcar/view/OutputViewTest.java @@ -0,0 +1,97 @@ +package racingcar.view; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutputViewTest { + + @Test + @DisplayName("자동차 위치를 - 기호로 포맷팅한다") + void formatCarPosition() { + // given + String carName = "pobi"; + int position = 3; + + // when + String result = OutputView.formatCarStatus(carName, position); + + // then + assertThat(result).isEqualTo("pobi : ---"); + } + + @ParameterizedTest + @CsvSource({ + "pobi, 0, 'pobi : '", + "pobi, 1, 'pobi : -'", + "pobi, 2, 'pobi : --'", + "pobi, 5, 'pobi : -----'" + }) + @DisplayName("다양한 위치를 정확히 포맷팅한다") + void formatVariousPositions(String carName, int position, String expected) { + // when + String result = OutputView.formatCarStatus(carName, position); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("단독 우승자를 포맷팅한다") + void formatSingleWinner() { + // given + List winners = List.of("pobi"); + + // when + String result = OutputView.formatWinners(winners); + + // then + assertThat(result).isEqualTo("최종 우승자 : pobi"); + } + + @Test + @DisplayName("공동 우승자를 쉼표로 구분하여 포맷팅한다") + void formatMultipleWinners() { + // given + List winners = Arrays.asList("pobi", "jun"); + + // when + String result = OutputView.formatWinners(winners); + + // then + assertThat(result).isEqualTo("최종 우승자 : pobi, jun"); + } + + @Test + @DisplayName("세 명 이상의 공동 우승자도 포맷팅한다") + void formatThreeWinners() { + // given + List winners = Arrays.asList("pobi", "woni", "jun"); + + // when + String result = OutputView.formatWinners(winners); + + // then + assertThat(result).isEqualTo("최종 우승자 : pobi, woni, jun"); + } + + @Test + @DisplayName("위치가 0인 자동차는 - 없이 이름만 출력한다") + void formatCarWithZeroPosition() { + // given + String carName = "pobi"; + int position = 0; + + // when + String result = OutputView.formatCarStatus(carName, position); + + // then + assertThat(result).isEqualTo("pobi : "); + } +} \ No newline at end of file