diff --git a/README.md b/README.md
index d0286c8..1b77aa0 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,103 @@
-# java-racingcar-precourse
+# java-racingCar-precourse
+
+
+> 콩 번째로 하는 우테코의 콩 번째 미션 \
+> 콩 번째로 하는 우테코의 콩 번째 미션
+
+
+ 과제 세부 내용
+
+## 과제 내용
+초간단 자동차 경주 게임을 구현한다.
+
+- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
+- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
+- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
+- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
+- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
+- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
+- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
+- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다.
+
+### 입출력
+- 입력
+ - 경주 할 자동차 이름
+ - 시도할 횟수
+- 출력
+ - 각 차수별 실행 결과
+ - 우승자 안내 문구
+
+ex)
+
+```
+경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
+pobi,woni,jun
+시도할 회수는 몇회인가요?
+5
+
+실행 결과
+pobi : -
+woni :
+jun : -
+
+pobi : --
+woni : -
+jun : --
+
+pobi : ---
+woni : --
+jun : ---
+
+pobi : ----
+woni : ---
+jun : ----
+
+pobi : -----
+woni : ----
+jun : -----
+
+최종 우승자 : pobi, jun
+```
+
+
+
+## 코드 흐름
+- 사용자의 이름 및 시도 횟수를 입력받는다. 이름의 유효성을 검증한다.
+- 랜덤으로 자동차의 전진 횟수를 더한다.
+- 최종 우승자를 출력한다.
+
+```mermaid
+sequenceDiagram
+ participant View
+ participant Controller
+ participant Model
+
+ Controller->>View: 입력 대기
+ View->>Controller: 이름, 시도횟수 반환
+ Controller->>Model: 이름, 시도횟수 전달
+ Model->>Controller: 게임 생성
+ loop 시도횟수만큼 반복
+ Controller->>Model: 실행 대기
+ Model->>Controller: 실행 결과 반환
+ Controller->>View: 실행 결과 전달
+ View->>View: 결과 출력
+ View->>Controller: -
+ end
+ Controller->>Model: 우승자 요청
+ Model->>Controller: 우승자 반환
+ Controller->>View: 우승자 전달
+ View->>View: 우승자 출력
+ View->>Controller: -
+ Controller->>Controller: 프로그램 종료
+
+```
+
+## 구현 기능 목록
+- 입출력
+ - [ ] 사용자 이름 입력
+ - [ ] 시도 횟수 입력
+ - [ ] 차수별 실행 결과 출력
+ - [ ] 최종 우승자 출력
+- 자동차 전진
+ - [ ] 여러 자동차의 상태 관리
+ - [ ] 랜덤 추출 기능
\ No newline at end of file
diff --git a/src/main/java/racingcar/AppConfig.java b/src/main/java/racingcar/AppConfig.java
new file mode 100644
index 0000000..d49c0f7
--- /dev/null
+++ b/src/main/java/racingcar/AppConfig.java
@@ -0,0 +1,40 @@
+package racingcar;
+
+import racingcar.controller.RacingcarController;
+import racingcar.service.CarControlService;
+import racingcar.service.RandomService;
+import racingcar.view.InputView;
+import racingcar.view.OutputView;
+import racingcar.view.provider.InputProvider;
+import racingcar.view.provider.WoowaInputProvider;
+
+public class AppConfig {
+ public InputProvider inputProvider() {
+ return new WoowaInputProvider();
+ }
+
+ public InputView inputView() {
+ return new InputView(inputProvider());
+ }
+
+ public OutputView outputView() {
+ return new OutputView();
+ }
+
+ public CarControlService carControlService() {
+ return new CarControlService();
+ }
+
+ public RandomService randomService() {
+ return new RandomService();
+ }
+
+ public RacingcarController racingcarController() {
+ return new RacingcarController(
+ inputView(),
+ outputView(),
+ carControlService(),
+ randomService()
+ );
+ }
+}
diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java
index a17a52e..51d3407 100644
--- a/src/main/java/racingcar/Application.java
+++ b/src/main/java/racingcar/Application.java
@@ -1,7 +1,16 @@
package racingcar;
+import racingcar.controller.RacingcarController;
+
public class Application {
+ static AppConfig appConfig;
+ static RacingcarController racingcarController;
+
public static void main(String[] args) {
// TODO: 프로그램 구현
+ appConfig = new AppConfig();
+ racingcarController = appConfig.racingcarController();
+
+ racingcarController.run();
}
}
diff --git a/src/main/java/racingcar/controller/RacingcarController.java b/src/main/java/racingcar/controller/RacingcarController.java
new file mode 100644
index 0000000..17e6b67
--- /dev/null
+++ b/src/main/java/racingcar/controller/RacingcarController.java
@@ -0,0 +1,55 @@
+package racingcar.controller;
+
+import java.util.List;
+import java.util.stream.IntStream;
+import racingcar.model.dto.CarMovementDto;
+import racingcar.service.CarControlService;
+import racingcar.service.RandomService;
+import racingcar.utils.Validator;
+import racingcar.view.InputView;
+import racingcar.view.OutputView;
+
+public class RacingcarController {
+ InputView inputView;
+ OutputView outputView;
+ CarControlService carControlService;
+ RandomService randomService;
+
+ private int trials;
+
+ public RacingcarController(InputView inputView, OutputView outputView,
+ CarControlService carControlService, RandomService randomService) {
+ this.inputView = inputView;
+ this.outputView = outputView;
+ this.carControlService = carControlService;
+ this.randomService = randomService;
+ }
+
+ public void run() {
+ initialize();
+ execute();
+ getResult();
+ }
+
+ private void initialize() {
+ String names = inputView.getCarName();
+ trials = inputView.getTrialNumber();
+ Validator.naturalNumberCheck(trials);
+ carControlService.initialize(names);
+ outputView.newLine();
+ }
+
+ private void execute() {
+ outputView.printResultGuide();
+ IntStream.range(0, trials).forEach((t) -> {
+ List progress = carControlService.playTurn(randomService::willCarMove);
+ outputView.printPlayerResult(progress);
+ outputView.newLine();
+ });
+ }
+
+ private void getResult() {
+ List winner = carControlService.getWinner();
+ outputView.printWinner(winner);
+ }
+}
diff --git a/src/main/java/racingcar/model/Car.java b/src/main/java/racingcar/model/Car.java
new file mode 100644
index 0000000..79eab81
--- /dev/null
+++ b/src/main/java/racingcar/model/Car.java
@@ -0,0 +1,21 @@
+package racingcar.model;
+
+import racingcar.model.dto.CarMovementDto;
+
+public class Car {
+ private final String name;
+ private int position;
+
+ public Car(String name) {
+ this.name = name;
+ position = 0;
+ }
+
+ public CarMovementDto getData() {
+ return new CarMovementDto(name, position);
+ }
+
+ public void move() {
+ position++;
+ }
+}
diff --git a/src/main/java/racingcar/model/Race.java b/src/main/java/racingcar/model/Race.java
new file mode 100644
index 0000000..ee6b08e
--- /dev/null
+++ b/src/main/java/racingcar/model/Race.java
@@ -0,0 +1,51 @@
+package racingcar.model;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Supplier;
+import racingcar.model.dto.CarMovementDto;
+
+public class Race {
+ private int turn;
+ private final List cars;
+
+ private Race() {
+ cars = new ArrayList<>();
+ }
+
+ public static Race init(List cars) {
+ Race race = new Race();
+ race.cars.addAll(cars.stream().map(Car::new).toList());
+ race.turn = 0;
+ return race;
+ }
+
+ public void advance(Supplier movementFunction) {
+ cars.forEach((car) -> {
+ if (movementFunction.get()) {
+ car.move();
+ }
+ });
+ turn++;
+ }
+
+ public List getResult() {
+ return cars.stream().map((Car::getData)).toList();
+ }
+
+ public List getWinner() {
+ int maxValue = cars.stream()
+ .map(car -> car.getData().movement())
+ .max(Comparator.naturalOrder()).orElse(0);
+
+ return cars.stream()
+ .filter(car -> car.getData().movement() == maxValue)
+ .map(car -> car.getData().name())
+ .toList();
+ }
+
+ public int getTurn() {
+ return turn;
+ }
+}
diff --git a/src/main/java/racingcar/model/dto/CarMovementDto.java b/src/main/java/racingcar/model/dto/CarMovementDto.java
new file mode 100644
index 0000000..31c8c18
--- /dev/null
+++ b/src/main/java/racingcar/model/dto/CarMovementDto.java
@@ -0,0 +1,16 @@
+package racingcar.model.dto;
+
+public record CarMovementDto(String name, int movement) {
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CarMovementDto that = (CarMovementDto) o;
+ return movement == that.movement && name.equals(that.name);
+ }
+}
diff --git a/src/main/java/racingcar/service/CarControlService.java b/src/main/java/racingcar/service/CarControlService.java
new file mode 100644
index 0000000..f665b0e
--- /dev/null
+++ b/src/main/java/racingcar/service/CarControlService.java
@@ -0,0 +1,40 @@
+package racingcar.service;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+import racingcar.model.Race;
+import racingcar.model.dto.CarMovementDto;
+import racingcar.utils.Validator;
+
+public class CarControlService {
+ private Race race;
+
+ private static final int MIN_CHAR_LENGTH = 1;
+ private static final int MAX_CHAR_LENGTH = 5;
+ private static final String COMMA = ",";
+
+ public void initialize(String names) {
+ List nameList = parseNames(names);
+ validateName(nameList);
+ this.race = Race.init(nameList);
+ }
+
+ private List parseNames(String names) {
+ return Arrays.stream(names.trim().split(COMMA)).toList();
+ }
+
+ public List playTurn(Supplier movementFunction) {
+ race.advance(movementFunction);
+ return race.getResult();
+ }
+
+ public List getWinner() {
+ return race.getWinner();
+ }
+
+ private void validateName(List nameList) {
+ nameList.forEach((name) -> Validator.stringRangeCheck(name, MIN_CHAR_LENGTH, MAX_CHAR_LENGTH));
+ Validator.duplicationCheck(nameList);
+ }
+}
diff --git a/src/main/java/racingcar/service/RandomService.java b/src/main/java/racingcar/service/RandomService.java
new file mode 100644
index 0000000..ec2c613
--- /dev/null
+++ b/src/main/java/racingcar/service/RandomService.java
@@ -0,0 +1,13 @@
+package racingcar.service;
+
+import camp.nextstep.edu.missionutils.Randoms;
+
+public class RandomService {
+ private static final int LOWER_BOUND = 0;
+ private static final int UPPER_BOUND = 9;
+ private static final int THRESHOLD = 4;
+
+ public boolean willCarMove() {
+ return Randoms.pickNumberInRange(LOWER_BOUND, UPPER_BOUND) >= THRESHOLD;
+ }
+}
diff --git a/src/main/java/racingcar/utils/MessageConstants.java b/src/main/java/racingcar/utils/MessageConstants.java
new file mode 100644
index 0000000..a81989e
--- /dev/null
+++ b/src/main/java/racingcar/utils/MessageConstants.java
@@ -0,0 +1,19 @@
+package racingcar.utils;
+
+public enum MessageConstants {
+ INPUT_CAR_NAME_GUIDE("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"),
+ INPUT_TRIAL_NUMBER_GUIDE("시도할 회수는 몇회인가요?"),
+ OUTPUT_EXECUTE_RESULT_GUIDE("실행 결과"),
+ OUTPUT_FINAL_WINNER_GUIDE("최종 우승자 : "),
+ OUTPUT_CAR_MOVEMENT("%s : ");
+
+ private final String message;
+
+ MessageConstants(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/racingcar/utils/Validator.java b/src/main/java/racingcar/utils/Validator.java
new file mode 100644
index 0000000..eacbc0a
--- /dev/null
+++ b/src/main/java/racingcar/utils/Validator.java
@@ -0,0 +1,26 @@
+package racingcar.utils;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class Validator {
+ public static void stringRangeCheck(String input, int min, int max) {
+ if (input.length() <= min || input.length() > max) {
+ throw new IllegalArgumentException("[Error] Invalid character length.");
+ }
+ }
+
+ public static void duplicationCheck(List itemList) {
+ Set uniqueNames = new HashSet<>(itemList);
+ if (uniqueNames.size() != itemList.size()) {
+ throw new IllegalArgumentException("[Error] Duplicated.");
+ }
+ }
+
+ public static void naturalNumberCheck(int num) {
+ if (num <= 0) {
+ throw new IllegalArgumentException("[Error] Invalid number.");
+ }
+ }
+}
diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java
new file mode 100644
index 0000000..6aecbb5
--- /dev/null
+++ b/src/main/java/racingcar/view/InputView.java
@@ -0,0 +1,30 @@
+package racingcar.view;
+
+import racingcar.utils.MessageConstants;
+import racingcar.view.provider.InputProvider;
+
+public class InputView {
+ private final InputProvider inputProvider;
+
+ public InputView(InputProvider inputProvider) {
+ this.inputProvider = inputProvider;
+ }
+
+ public String getCarName() {
+ try {
+ System.out.println(MessageConstants.INPUT_CAR_NAME_GUIDE.getMessage());
+ return inputProvider.readLine();
+ } catch (Exception exception) {
+ throw new IllegalArgumentException("[Error] 입출력 과정에서 예외가 발생했습니다. ");
+ }
+ }
+
+ public int getTrialNumber() {
+ try {
+ System.out.println(MessageConstants.INPUT_TRIAL_NUMBER_GUIDE.getMessage());
+ return Integer.parseInt(inputProvider.readLine());
+ } catch (Exception exception) {
+ throw new IllegalArgumentException("[Error] 입출력 과정에서 예외가 발생했습니다. ");
+ }
+ }
+}
diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java
new file mode 100644
index 0000000..353df7f
--- /dev/null
+++ b/src/main/java/racingcar/view/OutputView.java
@@ -0,0 +1,33 @@
+package racingcar.view;
+
+import java.util.List;
+import racingcar.model.dto.CarMovementDto;
+import racingcar.utils.MessageConstants;
+
+public class OutputView {
+ private final String SEPARATOR = ", ";
+ private final String MOVEMENT_DELIMITER = "-";
+
+ public void printResultGuide() {
+ System.out.println(MessageConstants.OUTPUT_EXECUTE_RESULT_GUIDE.getMessage());
+ }
+
+ public void printPlayerResult(List playerResult) {
+ playerResult.forEach((result) ->
+ System.out.println(String.format(
+ MessageConstants.OUTPUT_CAR_MOVEMENT.getMessage(), result.name()
+ ) + MOVEMENT_DELIMITER.repeat(result.movement()))
+ );
+ }
+
+ public void printWinner(List names) {
+ System.out.println(
+ MessageConstants.OUTPUT_FINAL_WINNER_GUIDE.getMessage()
+ + String.join(SEPARATOR, names)
+ );
+ }
+
+ public void newLine() {
+ System.out.println();
+ }
+}
diff --git a/src/main/java/racingcar/view/provider/InputProvider.java b/src/main/java/racingcar/view/provider/InputProvider.java
new file mode 100644
index 0000000..539c856
--- /dev/null
+++ b/src/main/java/racingcar/view/provider/InputProvider.java
@@ -0,0 +1,7 @@
+package racingcar.view.provider;
+
+import java.io.IOException;
+
+public interface InputProvider {
+ String readLine() throws IOException;
+}
diff --git a/src/main/java/racingcar/view/provider/WoowaInputProvider.java b/src/main/java/racingcar/view/provider/WoowaInputProvider.java
new file mode 100644
index 0000000..d1e1270
--- /dev/null
+++ b/src/main/java/racingcar/view/provider/WoowaInputProvider.java
@@ -0,0 +1,10 @@
+package racingcar.view.provider;
+
+import camp.nextstep.edu.missionutils.Console;
+
+public class WoowaInputProvider implements InputProvider {
+ @Override
+ public String readLine() {
+ return Console.readLine();
+ }
+}
diff --git a/src/test/java/racingcar/ApplicationTest.java b/src/test/java/racingcar/ApplicationTest.java
index 1d35fc3..f2c2495 100644
--- a/src/test/java/racingcar/ApplicationTest.java
+++ b/src/test/java/racingcar/ApplicationTest.java
@@ -1,12 +1,17 @@
package racingcar;
-import camp.nextstep.edu.missionutils.test.NsTest;
-import org.junit.jupiter.api.Test;
-
import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest;
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;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+
+import camp.nextstep.edu.missionutils.test.NsTest;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
class ApplicationTest extends NsTest {
private static final int MOVING_FORWARD = 4;
@@ -15,22 +20,204 @@ class ApplicationTest extends NsTest {
@Test
void 기능_테스트() {
assertRandomNumberInRangeTest(
- () -> {
- run("pobi,woni", "1");
- assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
- },
- MOVING_FORWARD, STOP
+ () -> {
+ run("pobi,woni", "1");
+ assertThat(output().trim()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
+ },
+ MOVING_FORWARD, STOP
);
}
@Test
void 예외_테스트() {
assertSimpleTest(() ->
- assertThatThrownBy(() -> runException("pobi,javaji", "1"))
- .isInstanceOf(IllegalArgumentException.class)
+ assertThatThrownBy(() -> runException("pobi,javaji", "1"))
+ .isInstanceOf(IllegalArgumentException.class)
);
}
+ @Nested
+ @DisplayName("Level 1")
+ class Level1 {
+ @Test
+ @DisplayName("기본 기능 테스트")
+ void basicTest() {
+ Integer[] numbers = Stream.concat(
+ Stream.of(MOVING_FORWARD, STOP, STOP),
+ Collections.nCopies(12, MOVING_FORWARD).stream()
+ ).toArray(Integer[]::new);
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("pobi,woni,jun", "5");
+ assertThat(output().trim()).contains(
+ "pobi : -----", "woni : ----", "jun : ----",
+ "최종 우승자 : pobi"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+
+ );
+ }
+
+ @Test
+ @DisplayName("랜덤 범위 테스트")
+ void randomRangeTest() {
+ Integer[] numbers = {0, 9, 1, 8, 2, 7, 3, 6, 4, 5};
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("loser,winer", "5");
+ assertThat(output().trim()).contains(
+ "loser : -", "winer : -----"
+ );
+ assertThat(output().trim()).doesNotContain(
+ "loser : --", "winer : ------"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+ );
+ }
+
+ @Test
+ @DisplayName("이름 길이 범위 테스트")
+ void longNameTest() {
+ assertSimpleTest(() -> {
+ assertThatThrownBy(() -> runException("java,script", "1"))
+ .isInstanceOf(IllegalArgumentException.class);
+ });
+ }
+ }
+
+ @Nested
+ @DisplayName("Level 2")
+ class Level2 {
+ @Test
+ @DisplayName("단일 우승자 테스트")
+ void singleWinnerTest() {
+ Integer[] numbers = {MOVING_FORWARD, STOP, STOP};
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("pobi,woni,jun", "1");
+ assertThat(output().trim()).contains(
+ "최종 우승자 : pobi"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+
+ );
+ }
+
+ @Test
+ @DisplayName("공동 우승자(2명) 테스트")
+ void doubleWinnerTest() {
+ Integer[] numbers = {MOVING_FORWARD, MOVING_FORWARD, STOP};
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("pobi,woni,jun", "1");
+ assertThat(output().trim()).contains(
+ "최종 우승자 : pobi, woni"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+
+ );
+ }
+
+ @Test
+ @DisplayName("공동 우승자(3명) 테스트")
+ void severalWinnerTest() {
+ Integer[] numbers = {MOVING_FORWARD, MOVING_FORWARD, MOVING_FORWARD};
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("pobi,woni,jun", "1");
+ assertThat(output().trim()).contains(
+ "최종 우승자 : pobi, woni, jun"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+
+ );
+ }
+ }
+
+ @Nested
+ @DisplayName("Level 3")
+ class Level3 {
+ @Test
+ @DisplayName("많은 시도횟수 테스트")
+ void veryBigTrialsTest() {
+ Integer[] numbers = Collections.nCopies(200, MOVING_FORWARD).toArray(Integer[]::new);
+
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("hogun,park", "100");
+ assertThat(output().trim()).contains(
+ "hogun : ----------------------------------------------------------------------------------------------------");
+ assertThat(output().trim()).contains(
+ "park : ----------------------------------------------------------------------------------------------------");
+ assertThat(output().trim()).doesNotContain(
+ "park : -----------------------------------------------------------------------------------------------------");
+ assertThat(output().trim()).doesNotContain(
+ "park : -----------------------------------------------------------------------------------------------------");
+
+ assertThat(output().trim()).contains(
+ "최종 우승자 : hogun, park"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+ );
+ }
+
+ @Test
+ @DisplayName("공백 이름 테스트")
+ void blankBetweenNameTest() {
+ Integer[] numbers = {MOVING_FORWARD, STOP};
+ assertRandomNumberInRangeTest(
+ () -> {
+ run("ja va,ji gi", "1");
+ assertThat(output().trim()).contains(
+ "최종 우승자 : ja va"
+ );
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+
+ );
+ }
+
+ @Test
+ @DisplayName("빈 입력 테스트 테스트")
+ void emptyAnswerTest() {
+ assertThatThrownBy(() -> run("", "1"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ @DisplayName("빈 이름 테스트")
+ void emptyNameTest() {
+ Integer[] numbers = Stream.concat(
+ Stream.of(STOP, MOVING_FORWARD, STOP),
+ Collections.nCopies(12, MOVING_FORWARD).stream()
+ ).toArray(Integer[]::new);
+ assertRandomNumberInRangeTest(
+ () -> {
+ try {
+ run("gamja,,yy", "1");
+ assertThat(output().trim()).contains("최종 우승자 : yy");
+ } catch (Exception exception) {
+ assertThat(exception).isInstanceOf(IllegalArgumentException.class);
+ }
+ }, numbers[0], Arrays.copyOfRange(numbers, 1, numbers.length)
+
+ );
+ }
+
+ @Test
+ @DisplayName("음수 시도 횟수 테스트")
+ void minusTrialTest() {
+ assertThatThrownBy(() -> run("pobi,jun", "-1"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ @DisplayName("이름 중복 테스트")
+ void duplicatedPlayerTest() {
+ assertThatThrownBy(() -> run("pobi,woni,pobi", "1"))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+ }
+
@Override
public void runMain() {
Application.main(new String[]{});
diff --git a/src/test/java/racingcar/MockInputProvider.java b/src/test/java/racingcar/MockInputProvider.java
new file mode 100644
index 0000000..9605d02
--- /dev/null
+++ b/src/test/java/racingcar/MockInputProvider.java
@@ -0,0 +1,21 @@
+package racingcar;
+
+import java.io.IOException;
+import racingcar.view.provider.InputProvider;
+
+public class MockInputProvider implements InputProvider {
+ private final String[] inputs;
+ private int index = 0;
+
+ public MockInputProvider(String... inputs) {
+ this.inputs = inputs;
+ }
+
+ @Override
+ public String readLine() throws IOException {
+ if (index < inputs.length) {
+ return inputs[index++];
+ }
+ throw new IOException();
+ }
+}
diff --git a/src/test/java/racingcar/MockRandomProvider.java b/src/test/java/racingcar/MockRandomProvider.java
new file mode 100644
index 0000000..4c77af1
--- /dev/null
+++ b/src/test/java/racingcar/MockRandomProvider.java
@@ -0,0 +1,17 @@
+package racingcar;
+
+public class MockRandomProvider {
+ private final boolean[] randomStack;
+ private int index = 0;
+
+ public MockRandomProvider(boolean... randomStack) {
+ this.randomStack = randomStack;
+ }
+
+ public boolean mockingRandom() {
+ if (index <= randomStack.length) {
+ return randomStack[index++];
+ }
+ throw new IllegalStateException();
+ }
+}
diff --git a/src/test/java/racingcar/ModelTest.java b/src/test/java/racingcar/ModelTest.java
new file mode 100644
index 0000000..0bc759b
--- /dev/null
+++ b/src/test/java/racingcar/ModelTest.java
@@ -0,0 +1,63 @@
+package racingcar;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.stream.IntStream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import racingcar.model.Race;
+import racingcar.model.dto.CarMovementDto;
+
+public class ModelTest {
+ private MockRandomProvider mockRandomProvider;
+
+ @BeforeEach
+ void setUp() {
+ mockRandomProvider = new MockRandomProvider(
+ true, false, true,
+ true, true, true,
+ true, true, true,
+ true, true, true,
+ true, true, true
+ );
+ }
+
+ @Nested
+ @DisplayName("경주 테스트")
+ class RaceTest {
+ @Test
+ @DisplayName("초기화 테스트")
+ public void initialTest() {
+ Race race = Race.init(List.of("pobi", "woni", "jun"));
+ assertEquals(0, race.getTurn());
+ assertEquals(
+ List.of(
+ new CarMovementDto("pobi", 0),
+ new CarMovementDto("woni", 0),
+ new CarMovementDto("jun", 0)
+ ),
+ race.getResult()
+ );
+ }
+
+ @Test
+ @DisplayName("이동 테스트")
+ public void movementTest() {
+ Race race = Race.init(List.of("pobi", "woni", "jun"));
+ IntStream.range(0, 5).forEach(i -> race.advance(mockRandomProvider::mockingRandom));
+ assertEquals(5, race.getTurn());
+ assertEquals(
+ List.of(
+ new CarMovementDto("pobi", 5),
+ new CarMovementDto("woni", 4),
+ new CarMovementDto("jun", 5)
+ ),
+ race.getResult()
+ );
+ assertEquals(List.of("pobi", "jun"), race.getWinner());
+ }
+ }
+}
diff --git a/src/test/java/racingcar/ViewTest.java b/src/test/java/racingcar/ViewTest.java
new file mode 100644
index 0000000..877a356
--- /dev/null
+++ b/src/test/java/racingcar/ViewTest.java
@@ -0,0 +1,90 @@
+package racingcar;
+
+import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import camp.nextstep.edu.missionutils.test.NsTest;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import racingcar.model.dto.CarMovementDto;
+import racingcar.view.InputView;
+import racingcar.view.OutputView;
+
+public class ViewTest extends NsTest {
+ private InputView inputView;
+ private OutputView outputView;
+
+ @BeforeEach
+ void setUp() {
+ inputView = new InputView(new MockInputProvider("pobi,woni,jun", "5"));
+ outputView = new OutputView();
+ }
+
+ @Nested
+ @DisplayName("입력 테스트")
+ class InputTest {
+ @Test
+ @DisplayName("차 이름 테스트")
+ void carNameInputTest() {
+ String carNames = inputView.getCarName();
+ assertEquals("pobi,woni,jun", carNames);
+ }
+
+ @Test
+ @DisplayName("시도 횟수 테스트")
+ void trialInputTest() {
+ inputView.getCarName();
+ int trialNumber = inputView.getTrialNumber();
+ assertEquals(5, trialNumber);
+ }
+ }
+
+ @Nested
+ @DisplayName("출력 테스트")
+ class OutputTest {
+ @Test
+ @DisplayName("결과 메시지 출력 테스트")
+ void printResultGuideTest() {
+ assertSimpleTest(() -> {
+ outputView.printResultGuide();
+ assertThat(output()).contains("실행 결과");
+ });
+ }
+
+ @Test
+ @DisplayName("경주 진행 출력 테스트")
+ void printPlayerResultTest() {
+ assertSimpleTest(() -> {
+ outputView.printPlayerResult(
+ List.of(
+ new CarMovementDto("pobi", 4),
+ new CarMovementDto("woni", 0),
+ new CarMovementDto("jun", 1)
+ )
+ );
+ assertThat(output()).isEqualTo("pobi : ----\nwoni : \njun : -");
+ });
+ }
+
+ @ParameterizedTest
+ @DisplayName("우승자 출력 테스트")
+ @CsvSource({"pobi", "pobi, jun", "pobi, woni, jun"})
+ void printWinnerTest(String winners) {
+ assertSimpleTest(() -> {
+ List winnerList = List.of(winners.split(", "));
+ outputView.printWinner(winnerList);
+ assertThat(output()).isEqualTo("최종 우승자 : " + winners);
+ });
+ }
+ }
+
+ @Override
+ public void runMain() {
+ }
+}