diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..1722da3deb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +- fix(controller): 객체 dto 변환 후 view에 전달하는 로직 추가 (62ceabb) +- fix(view): 객체 아닌 dto로 받아 출력하도록 수정 (1f0ba7f) +- fix(mapper): Mapper 클래스를 통한 객체 > dto 변환 로직 추가 (21b6292) +- fix(dto): dip 규칙 준수를 위해 cars 객체에 대한 dto 추가 (463939e) +- fix(dto): dip 규칙 준수를 위해 car 객체에 대한 dto 추가 (3ab8974) +- chore(CHCANGELOG): 작성한 커밋 메세지 전체 내용 추가 (05ad46f) +- test(service): RacingService 클래스 단위 테스트 추가 (6cf6583) +- test(domain): RandomMovePolicy 클래스 단위 테스트 추가 (67936d1) +- test(domain): WinnerCalculator 클래스 단위 테스트 추가 (abbe596) +- test(domain): Cars 클래스 단위 테스트 추가 (47b57ee) +- test(domain): Car 클래스 단위 테스트 추가 (d43a9f7) +- feat: 프로그램 실행을 위한 진입점 추가 (02a37a4) +- feat(error): IllegalArgumentException 발생 시 나오는 에러 메세지 추가 (3527baa) +- feat(controller): RacingController 클래스 구현 및 게임 실행 흐름 관리 (1026c07) +- feat(view): 사용자의 입출력 처리를 위한 view 클래스 구현 (3f5ce98) +- feat(view): 사용자에게 입출력 받기 위한 메세지 리소스 추가 (5f3238c) +- feat(service): RacingService 클래스 구현 및 게임 로직 제공 (dc4acfa) +- feat(domain): WinnerCalculator로 우승 자동차 판단 구현 (4e9b658) +- feat(domain): RandomMovePolicy로 이동 조건 구체화 (ad439ca) +- feat(util): 랜덤값 생성기 RandomGenerator 추가 (a92b8d7) +- feat(domain): MovePolicy 인터페이스 정의로 이동 조건 추상화 (5d3890d) +- feat(domain): Cars 일급 컬렉션 도입 및 리스트 관련 로직 캡슐화 (7c0a3c0) +- feat(domain): Car 클래스 구현 (9644837) +- docs(README): 기능 정리 및 예외 조건 등 요구사항 분석 README 추가 (5405b2d) \ No newline at end of file diff --git a/README.md b/README.md index d0286c859f..ae55bce3ac 100644 --- a/README.md +++ b/README.md @@ -1 +1,162 @@ # java-racingcar-precourse + +### 🔑 기능 정리 + +1. 사용자에게 자동차 경주할 자동차 이름들과 경주 차수를 받는다. +2. 경주 차수 동안 n대의 자동차가 조건에 따라, 전진 또는 멈춘 상황을 출력한다. +3. 게임이 끝난 후 가장 멀리 이동해 우승한 자동차를 출력한다. + +--- + +### ✅ 구현 조건 + +#### 입력 조건 + +자동차 이름 + +- [ ] 경주할 자동차 이름과 경주 차수 2가지 입력 필요 +- [ ] 자동차 이름은 `,` 기준으로 구분 +- [ ] 자동차 이름은 1자 이상 5자 이하만 가능함 +- [ ] 사용성을 고려해 자동차 이름에 공백이 있는 경우 없는 것으로 처리함 + +경주 차수 + +- [ ] 경주 차수는 숫자여야 함 +- [ ] 경주 차수는 양의 정수여야 함 +- [ ] 사용자가 잘못된 값을 입력한 경우 `IllegalArgumentException`을 발생 후 종료되어야 함 + +#### 동작 조건 + +- [ ] 경주는 1명이서 할 수 없음 +- [ ] 입력된 경주 차수 동안 n대의 자동차는 전진 또는 멈출 수 있음 + - [ ] 전진 조건: 0 ~ 9 사이에서 추출한 무작위 값이 4 이상인 경우 +- [ ] 각 경주마다 각 자동차의 이동 결과를 출력 +- [ ] 우승자는 한 명 이상일 수 있음 + - [ ] 우승 조건: 가장 많이 전진한 자동차 + +#### 출력 조건 + +- [ ] 자동차 이름은 `,` 기준으로 구분 +- [ ] 전진하는 자동차를 출력할 때마다, 전진한 자동차 이름과 이동거리를 출력 (ex `pobi : --`) +- [ ] 자동차 경주 게임 완료 후, 누가 우승했는지 출력 (ex `최종 우승자 : pobi`) +- [ ] 공동 우승자의 경우 `,`를 기준으로 출력 (ex `최종 우승자 : pobi, jun`) +- [ ] 우승자 출력 시, 입력 순서로 출력해야 함 + +--- + +### 🚨 예외(IllegalArgumentException) 발생 조건 + +- 잘못된 값을 입력한 경우 예외 발생 +- 입력해야 하는 값: `자동차 이름`, `경주 차수` + +자동차 이름 + +1. 자동차 이름이 공백이면 예외 발생 +2. 자동차 이름은 5자 초과면 예외 발생 +3. 자동차 이름 중복 입력 시 예외 발생 +4. 자동차 이름 목록에 이름이 1개일 경우 예외 발생 + +경주 차수 + +1. 경주 차수에 공백 입력 시 예외 발생 +2. 경주 차수에 숫자가 아닌 값 입력 시 예외 발생(`NumberFormatException`이 발생하는 경우) +3. 경주 차수에 0 입력 시 예외 발생 +4. 경주 차수에 음수 입력 시 예외 발생 + +--- + +### 🧪 테스트 목록 + +##### **CarTest** + +* [ ] 성공: 이름 길이 1~5자 허용, 생성 가능 +* [ ] 성공: 이동 조건 충족 시 위치 1 증가 +* [ ] 성공: 이동 조건 미충족 시 위치 유지 +* [ ] 실패: 이름이 공백이거나 5자 초과면 예외 발생 → `IllegalArgumentException` + +--- + +##### **CarsTest** + +* [ ] 성공: 여러 자동차 동시에 이동 가능 +* [ ] 성공: 라운드 진행 시 이동 거리 누적 +* [ ] 실패: 자동차 리스트 비어있음 → `IllegalArgumentException` +* [ ] 실패: 자동차 1대만 입력 → `IllegalArgumentException` +* [ ] 실패: 이름 중복 → `IllegalArgumentException` + +--- + +##### **RandomMovePolicyTest** + +* [ ] 성공: 랜덤값 4 이상이면 실제 정책에서 true 반환 가능 + +--- + +##### **WinnerCalculatorTest** + +* [ ] 성공: 최대 이동 거리 계산 및 우승자 필터링 +* [ ] 실패: 자동차 리스트 비어있음 → `IllegalArgumentException` + +--- + +##### **RacingServiceTest** + +* [ ] 성공: 자동차 이름 입력 후 trim 적용 +* [ ] 성공: 양의 정수 경주 차수 설정 가능 +* [ ] 성공: 여러 라운드 후 위치 누적 +* [ ] 성공: 우승자 리스트 반환, 입력 순서 유지 +* [ ] 실패: 자동차 이름 입력 null/공백 → `IllegalArgumentException` +* [ ] 실패: 경주 차수 0, 음수, 숫자 아님 → `IllegalArgumentException` + +--- + +### 📑 구현 순서 + +#### 1. 기본 세팅 + + - jdk 21 버전 설정 + - java style guide 설정 + +#### 2. 경주할 자동차 이름, 경주 차수 입력받기 + + - readLine() 으로 문자열 입력 받기 + - 경주할 자동차 이름 및 경주 차수 입력 유효성 검사 (예외 발생 조건 참고) + - 경주할 자동차 이름의 경우 , 기준으로 분할해 입력 처리하기 + +#### 3. 실행 결과 출력하기 + + - 0~9 사이의 수를 랜덤하게 뽑고, 4 이상인 경우 전진으로 처리(그 외는 멈춤으로 처리) + - 전진 여부 확인 후 전진한 횟수 증가시키기 + - 경주 차수만큼 실행 결과 출력하기 + +#### 4. 최종 우승자 발표하기 + + - 1명인 경우 그냥 출력 / 다수인 경우 , 로 구분하여 출력 + +--- + +### 📄 예시 입출력 + +``` +경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) +pobi,woni,jun +시도할 횟수는 몇 회인가요? +3 + +실행 결과 +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..16ad1a47ff 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,18 @@ package racingcar; +import racingcar.controller.RacingController; +import racingcar.domain.RandomMovePolicy; +import racingcar.service.RacingService; +import racingcar.view.InputView; +import racingcar.view.OutputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + RacingController controller = new RacingController( + new InputView(), + new OutputView(), + new RacingService(new RandomMovePolicy()) + ); + controller.run(); } } diff --git a/src/main/java/racingcar/controller/RacingController.java b/src/main/java/racingcar/controller/RacingController.java new file mode 100644 index 0000000000..481865d090 --- /dev/null +++ b/src/main/java/racingcar/controller/RacingController.java @@ -0,0 +1,48 @@ +package racingcar.controller; + +import racingcar.domain.Cars; +import racingcar.dto.CarsDto; +import racingcar.mapper.CarsMapper; +import racingcar.service.RacingService; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class RacingController { + + private final InputView inputView; + private final OutputView outputView; + private final RacingService racingService; + + public RacingController(InputView inputView, OutputView outputView, RacingService racingService) { + this.inputView = inputView; + this.outputView = outputView; + this.racingService = racingService; + } + + public void run() { + // 입력 값 받아오기 (경주할 자동차 이름, 경주 차수) + String namesInput = inputView.readCarNames(); + int roundCount = inputView.readRoundCount(); + + // 입력값 유효성 검증 + RacingService.validateInputNamesIsNotNullAndEmpty(namesInput); + RacingService.validateInputRoundCountIsPlus(roundCount); + + // 자동차 이름 값 검증 및 Cars 객체 생성 + racingService.initCars(namesInput); + + // 경주 차수에 따른 결과 출력 + outputView.printRoundResultTitle(); + for (int i = 0; i < roundCount; i++) { + // 변경된 cars 받아 dto로 변환 + Cars cars = racingService.playRound(); + CarsDto carsDto = CarsMapper.toDto(cars); + + // 경주 라운드 실행 결과 출력 + outputView.printRoundResult(carsDto); + } + + // 경주 승자 출력 + outputView.printWinners(racingService.getWinners()); + } +} diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..0411a934ca --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,42 @@ +package racingcar.domain; + +import static racingcar.error.ErrorMessages.CAR_NAME_SIZE_INVALID; + +public class Car { + private final String name; + private final int location; + + private static final int MAX_NAME_LENGTH = 5; + + public Car(String name) { + validateName(name); + this.name = name; + this.location = 0; + } + + private Car(String name, int location) { + this.name = name; + this.location = location; + } + + private void validateName(String name) { + if (name == null || name.isBlank() || name.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException(CAR_NAME_SIZE_INVALID.getMessage()); + } + } + + public Car move(boolean isMovable) { + if (isMovable) { + return new Car(name, location + 1); + } + return this; + } + + public String getName() { + return name; + } + + public int getLocation() { + return location; + } +} diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000000..e7442af7a2 --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,53 @@ +package racingcar.domain; + +import static racingcar.error.ErrorMessages.CARS_ARE_EMPTY; +import static racingcar.error.ErrorMessages.CARS_NAME_DUPLICATED; +import static racingcar.error.ErrorMessages.CARS_SIZE_INVALID; + +import java.util.List; +import java.util.stream.Collectors; + +public class Cars { + private final List cars; + + public Cars(List cars) { + validateListNotNullAndEmpty(cars); + validateInputNameIsMoreThanOne(cars); + validateUniqueNames(cars); + this.cars = cars; + } + + private static void validateListNotNullAndEmpty(List cars) { + if (cars == null || cars.isEmpty()) { + throw new IllegalArgumentException(CARS_ARE_EMPTY.getMessage()); + } + } + + private static void validateInputNameIsMoreThanOne(List cars) { + if (cars.size() < 2) { + throw new IllegalArgumentException(CARS_SIZE_INVALID.getMessage()); + } + } + + private static void validateUniqueNames(List cars) { + long distinctCount = cars.stream() + .map(Car::getName) + .distinct() + .count(); + + if (distinctCount != cars.size()) { + throw new IllegalArgumentException(CARS_NAME_DUPLICATED.getMessage()); + } + } + + public Cars moveAll(MovePolicy movePolicy) { + List moved = this.cars.stream() + .map(car -> car.move(movePolicy.canMove())) + .collect(Collectors.toList()); + return new Cars(moved); + } + + public List getCars() { + return cars; + } +} diff --git a/src/main/java/racingcar/domain/MovePolicy.java b/src/main/java/racingcar/domain/MovePolicy.java new file mode 100644 index 0000000000..c5bd48cca3 --- /dev/null +++ b/src/main/java/racingcar/domain/MovePolicy.java @@ -0,0 +1,5 @@ +package racingcar.domain; + +public interface MovePolicy { + boolean canMove(); +} diff --git a/src/main/java/racingcar/domain/RandomMovePolicy.java b/src/main/java/racingcar/domain/RandomMovePolicy.java new file mode 100644 index 0000000000..89f72af3a9 --- /dev/null +++ b/src/main/java/racingcar/domain/RandomMovePolicy.java @@ -0,0 +1,14 @@ +package racingcar.domain; + +import racingcar.util.RandomGenerator; + +public class RandomMovePolicy implements MovePolicy { + private static final int MIN = 0; + private static final int MAX = 9; + private static final int MIN_MOVE_TRIGGER = 4; + + @Override + public boolean canMove() { + return RandomGenerator.generate(MIN, MAX) >= MIN_MOVE_TRIGGER; + } +} diff --git a/src/main/java/racingcar/domain/WinnerCalculator.java b/src/main/java/racingcar/domain/WinnerCalculator.java new file mode 100644 index 0000000000..fe2ff4386b --- /dev/null +++ b/src/main/java/racingcar/domain/WinnerCalculator.java @@ -0,0 +1,21 @@ +package racingcar.domain; + +import static racingcar.error.ErrorMessages.CARS_ARE_EMPTY; + +import java.util.List; +import java.util.stream.Collectors; + +public class WinnerCalculator { + + public static List calculateWinners(Cars cars) { + int maxLocation = cars.getCars().stream() + .mapToInt(Car::getLocation) + .max() + .orElseThrow(() -> new IllegalArgumentException(CARS_ARE_EMPTY.getMessage())); + + return cars.getCars().stream() + .filter(car -> car.getLocation() == maxLocation) + .map(Car::getName) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/racingcar/dto/CarDto.java b/src/main/java/racingcar/dto/CarDto.java new file mode 100644 index 0000000000..d3def93c96 --- /dev/null +++ b/src/main/java/racingcar/dto/CarDto.java @@ -0,0 +1,5 @@ +package racingcar.dto; + +public record CarDto(String name, int location) { +} + diff --git a/src/main/java/racingcar/dto/CarsDto.java b/src/main/java/racingcar/dto/CarsDto.java new file mode 100644 index 0000000000..b2cafdfc00 --- /dev/null +++ b/src/main/java/racingcar/dto/CarsDto.java @@ -0,0 +1,14 @@ +package racingcar.dto; + +import java.util.List; + +public record CarsDto(List carDtoList) { + public CarsDto { + // 불변 리스트로 복사해서 저장 + carDtoList = List.copyOf(carDtoList); + } + + +} + + diff --git a/src/main/java/racingcar/error/ErrorMessages.java b/src/main/java/racingcar/error/ErrorMessages.java new file mode 100644 index 0000000000..46eaabf511 --- /dev/null +++ b/src/main/java/racingcar/error/ErrorMessages.java @@ -0,0 +1,23 @@ +package racingcar.error; + + +public enum ErrorMessages { + + ROUND_COUNT_INPUT_IS_NOT_PLUS("경주 차수는 양수여야 합니다."), + ROUND_COUNT_INPUT_MUST_VALID_NUMBER("경주 차수는 숫자여야 합니다."), + CAR_NAME_INPUT_IS_EMPTY("자동차 이름이 입력되지 않았습니다."), + CAR_NAME_SIZE_INVALID("자동차 이름은 1~5자여야 합니다."), + CARS_ARE_EMPTY("자동차 목록이 비어있습니다."), + CARS_SIZE_INVALID("자동차 이름은 두 개 이상이어야 합니다."), + CARS_NAME_DUPLICATED("자동차 이름이 중복됩니다."); + + private final String message; + + ErrorMessages(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/racingcar/mapper/CarsMapper.java b/src/main/java/racingcar/mapper/CarsMapper.java new file mode 100644 index 0000000000..51fe7e1b9c --- /dev/null +++ b/src/main/java/racingcar/mapper/CarsMapper.java @@ -0,0 +1,15 @@ +package racingcar.mapper; + +import java.util.List; +import racingcar.domain.Cars; +import racingcar.dto.CarDto; +import racingcar.dto.CarsDto; + +public class CarsMapper { + public static CarsDto toDto(Cars cars) { + List carDtoList = cars.getCars().stream() + .map(car -> new CarDto(car.getName(), car.getLocation())) + .toList(); + return new CarsDto(carDtoList); + } +} diff --git a/src/main/java/racingcar/service/RacingService.java b/src/main/java/racingcar/service/RacingService.java new file mode 100644 index 0000000000..a72a0a58d5 --- /dev/null +++ b/src/main/java/racingcar/service/RacingService.java @@ -0,0 +1,51 @@ +package racingcar.service; + +import static racingcar.error.ErrorMessages.CAR_NAME_INPUT_IS_EMPTY; +import static racingcar.error.ErrorMessages.ROUND_COUNT_INPUT_IS_NOT_PLUS; + +import java.util.Arrays; +import java.util.List; +import racingcar.domain.Car; +import racingcar.domain.Cars; +import racingcar.domain.MovePolicy; +import racingcar.domain.WinnerCalculator; + +public class RacingService { + public static final String NAME_SEPARATOR = ","; + private Cars cars; + private final MovePolicy movePolicy; + + public RacingService(MovePolicy movePolicy) { + this.movePolicy = movePolicy; + } + + public void initCars(String namesInput) { + List cars = Arrays.stream(namesInput.split(NAME_SEPARATOR)) + .map(String::trim) + .map(Car::new) + .toList(); + + this.cars = new Cars(cars); + } + + public static void validateInputNamesIsNotNullAndEmpty(String namesInput) { + if (namesInput == null || namesInput.isBlank()) { + throw new IllegalArgumentException(CAR_NAME_INPUT_IS_EMPTY.getMessage()); + } + } + + public static void validateInputRoundCountIsPlus(int roundCount) { + if (roundCount <= 0) { + throw new IllegalArgumentException(ROUND_COUNT_INPUT_IS_NOT_PLUS.getMessage()); + } + } + + public Cars playRound() { + cars = cars.moveAll(movePolicy); + return cars; + } + + public List getWinners() { + return WinnerCalculator.calculateWinners(cars); + } +} diff --git a/src/main/java/racingcar/util/RandomGenerator.java b/src/main/java/racingcar/util/RandomGenerator.java new file mode 100644 index 0000000000..10430ba0ab --- /dev/null +++ b/src/main/java/racingcar/util/RandomGenerator.java @@ -0,0 +1,10 @@ +package racingcar.util; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomGenerator { + + public static int generate(int min, int max) { + return Randoms.pickNumberInRange(min, max); + } +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..8f9f3c3a63 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,25 @@ +package racingcar.view; + +import static racingcar.error.ErrorMessages.ROUND_COUNT_INPUT_MUST_VALID_NUMBER; +import static racingcar.view.ViewMessages.INPUT_VIEW_QUESTION_CAR_NAMES; +import static racingcar.view.ViewMessages.INPUT_VIEW_QUESTION_ROUND_COUNT; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + public String readCarNames() { + System.out.println(INPUT_VIEW_QUESTION_CAR_NAMES); + return Console.readLine(); + } + + public int readRoundCount() { + System.out.println(INPUT_VIEW_QUESTION_ROUND_COUNT); + try { + return Integer.parseInt(Console.readLine()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ROUND_COUNT_INPUT_MUST_VALID_NUMBER.getMessage(), e); + } + } + +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..70dc40f804 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,26 @@ +package racingcar.view; + +import static racingcar.view.ViewMessages.OUTPUT_VIEW_FINAL_RESULT_WINNER; +import static racingcar.view.ViewMessages.OUTPUT_VIEW_FINAL_RESULT_WINNER_SEPARATOR; +import static racingcar.view.ViewMessages.OUTPUT_VIEW_ROUND_RESULT_MOVE_SYMBOL; +import static racingcar.view.ViewMessages.OUTPUT_VIEW_ROUND_RESULT_TITLE; + +import java.util.List; +import racingcar.dto.CarsDto; + +public class OutputView { + public void printRoundResultTitle() { + System.out.println(OUTPUT_VIEW_ROUND_RESULT_TITLE.getMessage()); + } + + public void printRoundResult(CarsDto cars) { + cars.carDtoList().forEach(car -> System.out.println(car.name() + " : " + + OUTPUT_VIEW_ROUND_RESULT_MOVE_SYMBOL.getMessage().repeat(car.location()))); + System.out.println(); + } + + public void printWinners(List winners) { + System.out.println(OUTPUT_VIEW_FINAL_RESULT_WINNER.getMessage() + + String.join(OUTPUT_VIEW_FINAL_RESULT_WINNER_SEPARATOR.getMessage(), winners)); + } +} diff --git a/src/main/java/racingcar/view/ViewMessages.java b/src/main/java/racingcar/view/ViewMessages.java new file mode 100644 index 0000000000..1d5b019943 --- /dev/null +++ b/src/main/java/racingcar/view/ViewMessages.java @@ -0,0 +1,22 @@ +package racingcar.view; + + +public enum ViewMessages { + + INPUT_VIEW_QUESTION_CAR_NAMES("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"), + INPUT_VIEW_QUESTION_ROUND_COUNT("시도할 횟수는 몇 회인가요?"), + OUTPUT_VIEW_ROUND_RESULT_TITLE("\n실행 결과"), + OUTPUT_VIEW_ROUND_RESULT_MOVE_SYMBOL("-"), + OUTPUT_VIEW_FINAL_RESULT_WINNER("최종 우승자 : "), + OUTPUT_VIEW_FINAL_RESULT_WINNER_SEPARATOR(", "); + + private final String message; + + ViewMessages(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000000..6b64c6e55a --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,45 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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; + +@DisplayName("Car 클래스 단위 테스트") +class CarTest { + + @Test + @DisplayName("성공: 이름 길이 1~5자면 생성 가능") + void 성공_이름_길이_1_5자_허용_생성_가능() { + assertThatCode(() -> new Car("pobi")) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("성공: 이동 조건 충족 시 위치 1 증가") + void 성공_이동_조건_충족시_위치_증가() { + Car car = new Car("pobi"); + Car moved = car.move(true); + assertThat(moved.getLocation()).isEqualTo(car.getLocation() + 1); + } + + @Test + @DisplayName("성공: 이동 조건 미충족 시 위치 유지") + void 성공_이동_조건_미충족시_위치_유지() { + Car car = new Car("pobi"); + Car stayed = car.move(false); + assertThat(stayed.getLocation()).isEqualTo(car.getLocation()); + } + + @ParameterizedTest(name = "이름이 \"{0}\"이면 IllegalArgumentException 발생") + @ValueSource(strings = {"", " ", "abcdef"}) + @DisplayName("실패: 이름이 공백이거나 5자 초과면 예외 발생") + void 실패_잘못된_이름_입력(String name) { + assertThatThrownBy(() -> new Car(name)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/racingcar/domain/CarsTest.java b/src/test/java/racingcar/domain/CarsTest.java new file mode 100644 index 0000000000..aa5269f30c --- /dev/null +++ b/src/test/java/racingcar/domain/CarsTest.java @@ -0,0 +1,61 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Cars 클래스 단위 테스트") +class CarsTest { + + static Cars cars; + + @BeforeAll + static void setup() { + cars = new Cars(List.of(new Car("a"), new Car("b"))); + } + + @Test + @DisplayName("성공: 여러 자동차 동시에 이동 가능") + void 성공_여러_자동차_동시에_이동_가능() { + MovePolicy movePolicy = () -> true; + Cars moved = cars.moveAll(movePolicy); + assertThat(moved.getCars()).allMatch(car -> car.getLocation() == 1); + } + + @Test + @DisplayName("성공: 라운드 진행 시 이동 거리 누적") + void 성공_라운드_진행시_이동_거리_누적() { + Cars cars = new Cars(List.of(new Car("a"), new Car("b"))); + MovePolicy movePolicy = () -> true; + + cars = cars.moveAll(movePolicy); + cars = cars.moveAll(movePolicy); + + assertThat(cars.getCars()).allMatch(car -> car.getLocation() == 2); + } + + @Test + @DisplayName("실패: 자동차 리스트 비어있으면 예외 발생") + void 실패_자동차_리스트_비어있으면_예외() { + assertThatThrownBy(() -> new Cars(List.of())) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("실패: 자동차 1대만 입력하면 예외 발생") + void 실패_자동차_1대만_입력하면_예외() { + assertThatThrownBy(() -> new Cars(List.of(new Car("solo")))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("실패: 자동차 이름 중복이면 예외 발생") + void 실패_자동차_이름_중복이면_예외() { + assertThatThrownBy(() -> new Cars(List.of(new Car("a"), new Car("a")))) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/racingcar/domain/RandomMovePolicyTest.java b/src/test/java/racingcar/domain/RandomMovePolicyTest.java new file mode 100644 index 0000000000..3a1a877d06 --- /dev/null +++ b/src/test/java/racingcar/domain/RandomMovePolicyTest.java @@ -0,0 +1,20 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; + +@DisplayName("RandomMovePolicy 단위 테스트") +class RandomMovePolicyTest { + + @RepeatedTest(5) + @DisplayName("랜덤값 4 이상이면 실제 정책에서 true 반환 가능") + void 성공_랜덤값_4이상_정책_전진_확률검증() { + RandomMovePolicy policy = new RandomMovePolicy(); + boolean result = policy.canMove(); + + // 4~9 값일 땐 true, 0~3 값일 땐 false — Random이므로 확률 테스트 + assertThat(result).isIn(true, false); + } +} diff --git a/src/test/java/racingcar/domain/WinnerCalculatorTest.java b/src/test/java/racingcar/domain/WinnerCalculatorTest.java new file mode 100644 index 0000000000..c2e886894f --- /dev/null +++ b/src/test/java/racingcar/domain/WinnerCalculatorTest.java @@ -0,0 +1,31 @@ +package racingcar.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("WinnerCalculator 단위 테스트") +class WinnerCalculatorTest { + + @Test + @DisplayName("성공: 최대 이동 거리 계산 및 우승자 필터링") + void 성공_최대_이동_거리_계산_및_우승자_필터링() { + Car car1 = new Car("pobi").move(true).move(true); + Car car2 = new Car("jun").move(true); + Cars cars = new Cars(List.of(car1, car2)); + + List winners = WinnerCalculator.calculateWinners(cars); + + assertThat(winners).containsExactly("pobi"); + } + + @Test + @DisplayName(" 실패: 자동차 리스트 비어있으면 예외 발생") + void 실패_자동차_리스트_비어있으면_예외() { + assertThatThrownBy(() -> WinnerCalculator.calculateWinners(new Cars(List.of()))) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/racingcar/service/RacingServiceTest.java b/src/test/java/racingcar/service/RacingServiceTest.java new file mode 100644 index 0000000000..702e55a221 --- /dev/null +++ b/src/test/java/racingcar/service/RacingServiceTest.java @@ -0,0 +1,82 @@ +package racingcar.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +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 racingcar.domain.Car; +import racingcar.domain.Cars; + +@DisplayName("RacingService 단위 테스트") +class RacingServiceTest { + + @Test + @DisplayName("성공: 자동차 이름 입력 시 trim 적용") + void 성공_자동차_이름_입력후_trim_적용() { + RacingService service = new RacingService(() -> true); + service.initCars(" pobi , jun "); + + List carNames = service.playRound().getCars() + .stream() + .map(Car::getName) + .toList(); + + assertThat(carNames).containsExactly("pobi", "jun"); + + } + + @Test + @DisplayName("성공: 양의 정수 경주 차수 설정 가능") + void 성공_양의_정수_경주_차수_설정_가능() { + assertThatCode(() -> RacingService.validateInputRoundCountIsPlus(3)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("성공: 여러 라운드 후 위치 누적") + void 성공_여러_라운드_후_위치_누적() { + RacingService service = new RacingService(() -> true); + service.initCars("pobi,jun"); + + service.playRound(); + Cars result = service.playRound(); + + assertThat(result.getCars()).allMatch(car -> car.getLocation() == 2); + } + + @Test + @DisplayName("성공: 우승자 리스트 반환 및 입력 순서 유지") + void 성공_우승자_리스트_반환_및_입력_순서_유지() { + RacingService service = new RacingService(() -> true); + service.initCars("pobi,jun"); + service.playRound(); + List winners = service.getWinners(); + + assertThat(winners).containsExactly("pobi", "jun"); + } + + @ParameterizedTest(name = "입력값 \"{0}\"이면 IllegalArgumentException 발생") + @NullAndEmptySource + @DisplayName("실패: 자동차 이름 입력 null 또는 공백이면 예외 발생") + void 실패_자동차_이름_입력_null_또는_공백(String input) { + assertThatThrownBy(() -> RacingService.validateInputNamesIsNotNullAndEmpty(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + + @ParameterizedTest(name = "입력값 \"{0}\"이면 IllegalArgumentException 발생") + @ValueSource(strings = {"0", "-1", "abc"}) + @DisplayName("실패: 경주 차수가 0, 음수, 숫자 아님이면 예외 발생") + void 실패_경주_차수_잘못된_입력(String input) { + assertThatThrownBy(() -> { + int roundCount = Integer.parseInt(input); // 숫자 변환 + RacingService.validateInputRoundCountIsPlus(roundCount); + }).isInstanceOf(IllegalArgumentException.class); + } +}