diff --git a/build.gradle b/build.gradle index 8172fb73f..12e06e7ea 100644 --- a/build.gradle +++ b/build.gradle @@ -3,15 +3,15 @@ apply plugin: 'eclipse' group = 'camp.nextstep' version = '1.0.0' -sourceCompatibility = '1.8' +sourceCompatibility = '11' repositories { mavenCentral() } dependencies { - testImplementation "org.junit.jupiter:junit-jupiter:5.7.2" - testImplementation "org.assertj:assertj-core:3.19.0" + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testImplementation 'org.assertj:assertj-core:3.21.0' } test { diff --git a/src/main/java/racing/RacingCarApplication.java b/src/main/java/racing/RacingCarApplication.java new file mode 100644 index 000000000..aef77b26b --- /dev/null +++ b/src/main/java/racing/RacingCarApplication.java @@ -0,0 +1,23 @@ +package racing; + +import racing.controller.GameController; +import racing.strategy.MovementStrategy; +import racing.strategy.RandomMovementStrategyStrategy; +import racing.view.InputConsoleView; +import racing.view.InputView; +import racing.view.OutputConsoleView; +import racing.view.OutputView; + +import java.util.Scanner; + +public class RacingCarApplication { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + InputView inputView = new InputConsoleView(scanner); + OutputView outputView = new OutputConsoleView(); + MovementStrategy movementStrategy = new RandomMovementStrategyStrategy(); + GameController gameController = new GameController(inputView, outputView, movementStrategy); + gameController.run(); + scanner.close(); + } +} diff --git a/src/main/java/racing/controller/GameController.java b/src/main/java/racing/controller/GameController.java new file mode 100644 index 000000000..8cf7559e9 --- /dev/null +++ b/src/main/java/racing/controller/GameController.java @@ -0,0 +1,62 @@ +package racing.controller; + +import racing.domain.Car; +import racing.domain.Cars; +import racing.domain.Names; +import racing.domain.Round; +import racing.domain.vo.NumberOfAttempt; +import racing.dto.InputCarNameDto; +import racing.dto.InputNumberOfAttemptDto; +import racing.dto.OutputRoundResultDto; +import racing.dto.OutputWinnerDto; +import racing.strategy.MovementStrategy; +import racing.view.InputView; +import racing.view.OutputView; + +import java.util.List; + +public class GameController { + private final InputView inputView; + private final OutputView outputView; + private final MovementStrategy movementStrategy; + + public GameController(final InputView inputView, final OutputView outputView, final MovementStrategy movementStrategy) { + this.inputView = inputView; + this.outputView = outputView; + this.movementStrategy = movementStrategy; + } + + public void run() { + Round round = initRound(); + outputView.printRoundResultMessage(); + round = race(round); + printWinner(round); + } + + private Round race(final Round initialRound) { + Round round = initialRound; + + while (round.isPlaying()) { + round = round.play(movementStrategy); + Cars nextRoundCars = round.cars(); + OutputRoundResultDto outputRoundResultDto = new OutputRoundResultDto(nextRoundCars); + outputView.printRoundResult(outputRoundResultDto); + } + + return round; + } + + private Round initRound() { + InputCarNameDto inputCarNameDto = inputView.inputCarNames(); + InputNumberOfAttemptDto inputNumberOfAttemptDto = inputView.inputNumberOfAttempt(); + Names names = inputCarNameDto.toNames(); + NumberOfAttempt numberOfAttempt = inputNumberOfAttemptDto.toNumberOfAttempt(); + return new Round(names, numberOfAttempt); + } + + private void printWinner(final Round round) { + List winners = round.winners(); + OutputWinnerDto outputWinnerDto = new OutputWinnerDto(winners); + outputView.printWinner(outputWinnerDto); + } +} diff --git a/src/main/java/racing/domain/Car.java b/src/main/java/racing/domain/Car.java new file mode 100644 index 000000000..c4512a089 --- /dev/null +++ b/src/main/java/racing/domain/Car.java @@ -0,0 +1,62 @@ +package racing.domain; + +import racing.domain.vo.Name; +import racing.domain.vo.Position; +import racing.strategy.MovementStrategy; + +import java.util.Objects; + +public class Car { + + private static final int INITIAL_LOCATION = 0; + private static final int DISTANCE_MOVED_AT_ONCE = 1; + + private final Name name; + private final Position position; + + public Car(final Name name, final Position position) { + this.name = name; + this.position = position; + } + + public Car(final String name, final int position) { + this(new Name(name), new Position(position)); + } + + public Car(final String name) { + this(name, INITIAL_LOCATION); + } + + public Car(final Name name) { + this(name, new Position(INITIAL_LOCATION)); + } + + public String name() { + return name.value(); + } + + public int position() { + return position.value(); + } + + public Car move(MovementStrategy movementStrategy) { + if (movementStrategy.canMove()) { + return new Car(name, position.increaseBy(DISTANCE_MOVED_AT_ONCE)); + } + + return this; + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (!(other instanceof Car)) return false; + final Car car = (Car) other; + return name.equals(car.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/src/main/java/racing/domain/Cars.java b/src/main/java/racing/domain/Cars.java new file mode 100644 index 000000000..c0ed29533 --- /dev/null +++ b/src/main/java/racing/domain/Cars.java @@ -0,0 +1,52 @@ +package racing.domain; + +import racing.strategy.MovementStrategy; + +import java.util.List; +import java.util.stream.Collectors; + +public class Cars { + + private final List elements; + + public Cars(final List elements) { + this.elements = elements; + } + + public Cars(final Names carNames) { + this(convertFrom(carNames)); + } + + private static List convertFrom(final Names carNames) { + return carNames.elements() + .stream() + .map(Car::new) + .collect(Collectors.toUnmodifiableList()); + + } + + public List elements() { + return elements; + } + + public Cars driveAll(final MovementStrategy movementStrategy) { + List droveCar = elements.stream() + .map(car -> car.move(movementStrategy)) + .collect(Collectors.toUnmodifiableList()); + return new Cars(droveCar); + } + + public List findMaxPositionCars() { + int max = findMaxPosition(); + return elements.stream() + .filter(car -> car.position() == max) + .collect(Collectors.toUnmodifiableList()); + } + + private int findMaxPosition() { + return elements.stream() + .map(Car::position) + .max(Integer::compareTo) + .orElseThrow(() -> new IllegalArgumentException("최대값을 찾을 수 없습니다")); + } +} diff --git a/src/main/java/racing/domain/Names.java b/src/main/java/racing/domain/Names.java new file mode 100644 index 000000000..54b658c5d --- /dev/null +++ b/src/main/java/racing/domain/Names.java @@ -0,0 +1,18 @@ +package racing.domain; + +import racing.domain.vo.Name; + +import java.util.List; + +public class Names { + + private final List elements; + + public Names(final List elements) { + this.elements = elements; + } + + public List elements() { + return elements; + } +} diff --git a/src/main/java/racing/domain/Round.java b/src/main/java/racing/domain/Round.java new file mode 100644 index 000000000..04f3dd7c9 --- /dev/null +++ b/src/main/java/racing/domain/Round.java @@ -0,0 +1,39 @@ +package racing.domain; + +import racing.domain.vo.NumberOfAttempt; +import racing.strategy.MovementStrategy; + +import java.util.List; + +public class Round { + + private final Cars cars; + private final NumberOfAttempt numberOfAttempt; + + public Round(final Cars cars, final NumberOfAttempt numberOfAttempt) { + this.cars = cars; + this.numberOfAttempt = numberOfAttempt; + } + + public Round(final Names names, final NumberOfAttempt numberOfAttempt) { + this(new Cars(names), numberOfAttempt); + } + + public Round play(final MovementStrategy movementStrategy) { + NumberOfAttempt numberOfAttemptAfterTrying = numberOfAttempt.decrease(); + Cars carsAfterDriveAll = this.cars.driveAll(movementStrategy); + return new Round(carsAfterDriveAll, numberOfAttemptAfterTrying); + } + + public Cars cars() { + return cars; + } + + public List winners() { + return cars.findMaxPositionCars(); + } + + public boolean isPlaying() { + return !numberOfAttempt.isOver(); + } +} diff --git a/src/main/java/racing/domain/vo/Name.java b/src/main/java/racing/domain/vo/Name.java new file mode 100644 index 000000000..d9bf95661 --- /dev/null +++ b/src/main/java/racing/domain/vo/Name.java @@ -0,0 +1,39 @@ +package racing.domain.vo; + +import java.util.Objects; + +public class Name { + + private static final int MIN_NAME_LENGTH = 1; + private static final int MAX_NAME_LENGTH = 5; + + private final String value; + + public Name(final String value) { + validate(value); + this.value = value; + } + + private void validate(final String value) { + if (value.length() < MIN_NAME_LENGTH || value.length() > MAX_NAME_LENGTH) { + throw new IllegalArgumentException("이름이 길이는 1이상 5이하여야 합니다"); + } + } + + public String value() { + return value; + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (!(other instanceof Name)) return false; + final Name name = (Name) other; + return value.equals(name.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/racing/domain/vo/NumberOfAttempt.java b/src/main/java/racing/domain/vo/NumberOfAttempt.java new file mode 100644 index 000000000..5de214627 --- /dev/null +++ b/src/main/java/racing/domain/vo/NumberOfAttempt.java @@ -0,0 +1,44 @@ +package racing.domain.vo; + +import java.util.Objects; + +public class NumberOfAttempt { + + private static final int ZERO = 0; + private static final int SUBTRACTED_VALUE = 1; + + private final int value; + + public NumberOfAttempt(final int value) { + if (value < ZERO) { + throw new IllegalArgumentException("시도 횟수는 0 이상의 수 입니다"); + } + + this.value = value; + } + + public int value() { + return value; + } + + public NumberOfAttempt decrease() { + return new NumberOfAttempt(value - SUBTRACTED_VALUE); + } + + public boolean isOver() { + return value == ZERO; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof NumberOfAttempt)) return false; + final NumberOfAttempt that = (NumberOfAttempt) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/racing/domain/vo/Position.java b/src/main/java/racing/domain/vo/Position.java new file mode 100644 index 000000000..f5a9b8582 --- /dev/null +++ b/src/main/java/racing/domain/vo/Position.java @@ -0,0 +1,33 @@ +package racing.domain.vo; + +import java.util.Objects; + +public class Position { + + private final int value; + + public Position(final int value) { + this.value = value; + } + + public int value() { + return value; + } + + public Position increaseBy(final int distanceMovedAtOnce) { + return new Position(value + distanceMovedAtOnce); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof Position)) return false; + final Position position = (Position) o; + return value == position.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/racing/dto/InputCarNameDto.java b/src/main/java/racing/dto/InputCarNameDto.java new file mode 100644 index 000000000..bfb9a25e3 --- /dev/null +++ b/src/main/java/racing/dto/InputCarNameDto.java @@ -0,0 +1,41 @@ +package racing.dto; + +import racing.domain.Names; +import racing.domain.vo.Name; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class InputCarNameDto { + private final List names; + + public InputCarNameDto(final List names) { + this.names = names; + } + + public InputCarNameDto(final String[] names) { + this(parseFrom(names)); + } + + private static List parseFrom(final String[] carNames) { + List names = Arrays.stream(carNames) + .distinct() + .collect(Collectors.toUnmodifiableList()); + validateDuplicatedName(carNames, names); + return names; + } + + private static void validateDuplicatedName(final String[] carNames, final List names) { + if (names.size() < carNames.length) { + throw new IllegalArgumentException("중복된 이름이 존재합니다"); + } + } + + public Names toNames() { + List elements = names.stream() + .map(Name::new) + .collect(Collectors.toList()); + return new Names(elements); + } +} diff --git a/src/main/java/racing/dto/InputNumberOfAttemptDto.java b/src/main/java/racing/dto/InputNumberOfAttemptDto.java new file mode 100644 index 000000000..7b9989b57 --- /dev/null +++ b/src/main/java/racing/dto/InputNumberOfAttemptDto.java @@ -0,0 +1,29 @@ +package racing.dto; + +import racing.domain.vo.NumberOfAttempt; + +public class InputNumberOfAttemptDto { + private final int numberOfAttempt; + + public InputNumberOfAttemptDto(final int numberOfAttempt) { + this.numberOfAttempt = numberOfAttempt; + } + + public InputNumberOfAttemptDto(final String numberOfAttempt) { + this(parseFrom(numberOfAttempt)); + } + + private static int parseFrom(final String numberOfAttempt) { + boolean isNotDigit = numberOfAttempt.chars() + .anyMatch(num -> !Character.isDigit(num)); + if (isNotDigit) { + throw new IllegalArgumentException("올바른 시도 횟수가 아닙니다"); + } + + return Integer.parseInt(numberOfAttempt); + } + + public NumberOfAttempt toNumberOfAttempt() { + return new NumberOfAttempt(numberOfAttempt); + } +} diff --git a/src/main/java/racing/dto/OutputRoundResultDto.java b/src/main/java/racing/dto/OutputRoundResultDto.java new file mode 100644 index 000000000..27e21695b --- /dev/null +++ b/src/main/java/racing/dto/OutputRoundResultDto.java @@ -0,0 +1,18 @@ +package racing.dto; + +import racing.domain.Car; +import racing.domain.Cars; + +import java.util.List; + +public class OutputRoundResultDto { + private final List cars; + + public OutputRoundResultDto(final Cars cars) { + this.cars = cars.elements(); + } + + public List cars() { + return cars; + } +} diff --git a/src/main/java/racing/dto/OutputWinnerDto.java b/src/main/java/racing/dto/OutputWinnerDto.java new file mode 100644 index 000000000..4f7fc298f --- /dev/null +++ b/src/main/java/racing/dto/OutputWinnerDto.java @@ -0,0 +1,24 @@ +package racing.dto; + +import racing.domain.Car; + +import java.util.List; +import java.util.stream.Collectors; + +public class OutputWinnerDto { + private final List winners; + + public OutputWinnerDto(final List winners) { + this.winners = convertFrom(winners); + } + + private List convertFrom(final List winners) { + return winners.stream() + .map(Car::name) + .collect(Collectors.toUnmodifiableList()); + } + + public List winners() { + return winners; + } +} diff --git a/src/main/java/racing/strategy/MovementStrategy.java b/src/main/java/racing/strategy/MovementStrategy.java new file mode 100644 index 000000000..a4ce5684e --- /dev/null +++ b/src/main/java/racing/strategy/MovementStrategy.java @@ -0,0 +1,5 @@ +package racing.strategy; + +public interface MovementStrategy { + boolean canMove(); +} diff --git a/src/main/java/racing/strategy/RandomMovementStrategyStrategy.java b/src/main/java/racing/strategy/RandomMovementStrategyStrategy.java new file mode 100644 index 000000000..2c9839385 --- /dev/null +++ b/src/main/java/racing/strategy/RandomMovementStrategyStrategy.java @@ -0,0 +1,17 @@ +package racing.strategy; + +import java.util.Random; + +public class RandomMovementStrategyStrategy implements MovementStrategy { + + private final Random random = new Random(); + + @Override + public boolean canMove() { + return generateRandom(); + } + + protected boolean generateRandom() { + return random.nextBoolean(); + } +} diff --git a/src/main/java/racing/view/InputConsoleView.java b/src/main/java/racing/view/InputConsoleView.java new file mode 100644 index 000000000..470565257 --- /dev/null +++ b/src/main/java/racing/view/InputConsoleView.java @@ -0,0 +1,33 @@ +package racing.view; + +import racing.dto.InputCarNameDto; +import racing.dto.InputNumberOfAttemptDto; + +import java.util.Scanner; + +import static java.lang.System.out; + +public class InputConsoleView implements InputView { + + private static final String NAME_TOKEN_DELIMITER = "\\s*,\\s*"; + private final Scanner scanner; + + public InputConsoleView(final Scanner scanner) { + this.scanner = scanner; + } + + @Override + public InputCarNameDto inputCarNames() { + out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + String nameToken = scanner.nextLine(); + String[] names = nameToken.split(NAME_TOKEN_DELIMITER); + return new InputCarNameDto(names); + } + + @Override + public InputNumberOfAttemptDto inputNumberOfAttempt() { + out.println("시도할 회수는 몇회인가요?"); + String numberValue = scanner.nextLine(); + return new InputNumberOfAttemptDto(numberValue); + } +} diff --git a/src/main/java/racing/view/InputView.java b/src/main/java/racing/view/InputView.java new file mode 100644 index 000000000..7952dd6ef --- /dev/null +++ b/src/main/java/racing/view/InputView.java @@ -0,0 +1,11 @@ +package racing.view; + +import racing.dto.InputCarNameDto; +import racing.dto.InputNumberOfAttemptDto; + +public interface InputView { + + InputCarNameDto inputCarNames(); + + InputNumberOfAttemptDto inputNumberOfAttempt(); +} diff --git a/src/main/java/racing/view/OutputConsoleView.java b/src/main/java/racing/view/OutputConsoleView.java new file mode 100644 index 000000000..d2cb68ec5 --- /dev/null +++ b/src/main/java/racing/view/OutputConsoleView.java @@ -0,0 +1,50 @@ +package racing.view; + +import racing.domain.Car; +import racing.dto.OutputRoundResultDto; +import racing.dto.OutputWinnerDto; + +import java.util.List; +import java.util.StringJoiner; +import java.util.stream.IntStream; + +import static java.lang.System.out; + +public class OutputConsoleView implements OutputView { + + private static final String DISTANCE_SYMBOL = "-"; + private static final String WINNER_NAME_DELIMITER = ", "; + + @Override + public void printRoundResultMessage() { + out.println(); + out.println("실행결과"); + } + + @Override + public void printRoundResult(final OutputRoundResultDto outputRoundResultDto) { + List cars = outputRoundResultDto.cars(); + cars.forEach(car -> out.println(car.name() + getDistanceSymbols(car.position()))); + out.println(); + } + + private String getDistanceSymbols(final int position) { + StringBuilder distanceSymbols = new StringBuilder(); + IntStream.range(0, position) + .forEach(index -> distanceSymbols.append(DISTANCE_SYMBOL)); + return distanceSymbols.toString(); + } + + @Override + public void printWinner(final OutputWinnerDto outputWinnerDto) { + List winners = outputWinnerDto.winners(); + String winnerName = convertWinnerNameFrom(winners); + out.println(winnerName + "가 최종 우승했습니다."); + } + + private String convertWinnerNameFrom(final List winners) { + StringJoiner winnerName = new StringJoiner(WINNER_NAME_DELIMITER); + winners.forEach(winnerName::add); + return winnerName.toString(); + } +} diff --git a/src/main/java/racing/view/OutputView.java b/src/main/java/racing/view/OutputView.java new file mode 100644 index 000000000..6c53543e4 --- /dev/null +++ b/src/main/java/racing/view/OutputView.java @@ -0,0 +1,12 @@ +package racing.view; + +import racing.dto.OutputRoundResultDto; +import racing.dto.OutputWinnerDto; + +public interface OutputView { + void printRoundResultMessage(); + + void printRoundResult(final OutputRoundResultDto outputRoundResultDto); + + void printWinner(final OutputWinnerDto outputWinnerDto); +} diff --git a/src/test/java/racing/domain/CarTest.java b/src/test/java/racing/domain/CarTest.java new file mode 100644 index 000000000..de782e667 --- /dev/null +++ b/src/test/java/racing/domain/CarTest.java @@ -0,0 +1,79 @@ +package racing.domain; + +import org.junit.jupiter.api.BeforeEach; +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 racing.strategy.MovementStrategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class CarTest { + private Car car; + + @BeforeEach + void setUp() { + car = new Car("name"); + } + + @Test + @DisplayName("이름를 입력받아 자동차 생성해서 이름과 초기 위치를 반환한다") + void create() { + //given + String name = car.name(); + int position = car.position(); + + //then + assertAll( + () -> assertThat(name).isEqualTo("name"), + () -> assertThat(position).isZero() + ); + } + + @ParameterizedTest + @CsvSource(value = {"true,1", "false,0"}) + @DisplayName("차의 위치를 증가시킨다") + void move(boolean randomValue, int expected) { + //given + MovementStrategy movementStrategy = () -> randomValue; + Car actualCar = car.move(movementStrategy); + + //when + int actual = actualCar.position(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"name,name,true", "name,other,false"}) + @DisplayName("차의 이름이 같으면 참, 다른 이름일 경우 거짓을 반환한다") + void equals(String name, String otherName, boolean expected) { + //given + Car car = new Car(name); + Car other = new Car(otherName); + + //when + boolean actual = car.equals(other); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"name,name,true", "name,other,false"}) + @DisplayName("차의 이름이 같으면 참, 다른 이름일 경우 거짓을 반환한다") + void hash(String name, String otherName, boolean expected) { + //given + Car car = new Car(name); + Car other = new Car(otherName); + + //when + boolean actual = car.hashCode() == other.hashCode(); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/racing/domain/CarsTest.java b/src/test/java/racing/domain/CarsTest.java new file mode 100644 index 000000000..7def80759 --- /dev/null +++ b/src/test/java/racing/domain/CarsTest.java @@ -0,0 +1,91 @@ +package racing.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.CsvSource; +import racing.domain.vo.Name; +import racing.strategy.MovementStrategy; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +class CarsTest { + + @Test + @DisplayName("차 이름들을 인자로 받아 초기화한다") + void create_with_car_names() { + //given + List names = + Arrays.asList(new Name("name1"), new Name("name2"), new Name("name3")); + Names carNames = new Names(names); + Cars cars = new Cars(carNames); + + //when + List actual = cars.elements(); + + //then + assertThat(actual).extracting("name.value", "position.value") + .containsExactly( + tuple("name1", 0), + tuple("name2", 0), + tuple("name3", 0)); + } + + @Test + @DisplayName("차 요소들로 초기화 한다") + void create_with_cars() { + //given + List elements = Arrays.asList(new Car("name1", 1), + new Car("name2", 2), new Car("name3", 3)); + Cars cars = new Cars(elements); + + //when + List actual = cars.elements(); + + //then + assertThat(actual).extracting("name.value", "position.value") + .containsExactly( + tuple("name1", 1), + tuple("name2", 2), + tuple("name3", 3)); + } + + @ParameterizedTest + @CsvSource(value = {"true, 2", "false, 1"}) + @DisplayName("모든 차를 운행시킨다") + void driveAll(final boolean randomValue, final int expectedPosition) { + //given + List elements = List.of(new Car("name1", 1)); + Cars cars = new Cars(elements); + MovementStrategy movementStrategy = () -> randomValue; + + //when + Cars droveCars = cars.driveAll(movementStrategy); + + //then + assertThat(droveCars.elements()).extracting("position.value") + .containsExactly(expectedPosition); + } + + @Test + @DisplayName("위치가 가장 높은 차를 반환한다") + void findMaxPositionCars() { + //given + List elements = Arrays.asList(new Car("name1", 2), new Car("name2", 2), + new Car("name3", 1)); + Cars cars = new Cars(elements); + + //when + List maxPositionCars = cars.findMaxPositionCars(); + + //then + assertThat(maxPositionCars).extracting("name.value", "position.value") + .containsExactly( + tuple("name1", 2), + tuple("name2", 2)); + } +} diff --git a/src/test/java/racing/domain/NamesTest.java b/src/test/java/racing/domain/NamesTest.java new file mode 100644 index 000000000..4a8b3cbb9 --- /dev/null +++ b/src/test/java/racing/domain/NamesTest.java @@ -0,0 +1,27 @@ +package racing.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.domain.vo.Name; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class NamesTest { + + @Test + @DisplayName("이름 객체 리스트를 입력받아 이름 일급 컬렉션을 생성한다") + void getElements() { + //given + List expected = Arrays.asList(new Name("name1"), new Name("name2")); + Names names = new Names(expected); + + //when + List actual = names.elements(); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/racing/domain/RoundTest.java b/src/test/java/racing/domain/RoundTest.java new file mode 100644 index 000000000..a27f80790 --- /dev/null +++ b/src/test/java/racing/domain/RoundTest.java @@ -0,0 +1,129 @@ +package racing.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.CsvSource; +import racing.domain.vo.NumberOfAttempt; +import racing.strategy.MovementStrategy; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.api.Assertions.assertAll; + +class RoundTest { + + @Test + @DisplayName("시도 횟수와 차들을 인자로 받아 한 라운드를 생성한다") + void create() { + //given + int value = 1; + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(value); + List elements = Arrays.asList(new Car("name1"), new Car("name2")); + Cars cars = new Cars(elements); + + //when + Round round = new Round(cars, numberOfAttempt); + + //then + assertAll( + () -> assertThat(round).extracting("cars.elements").isEqualTo(elements), + () -> assertThat(round).extracting("numberOfAttempt.value").isEqualTo(value) + ); + + } + + @Test + @DisplayName("움직이는 전략을 인자로 받아 한 라운드를 수행하고 움직인 차들과 시도 횟수가 차감된 새로운 라운드를 반환한다.") + void play() { + //given + int numberOfAttemptValue = 2; + int expectedNumberOfAttemptValue = numberOfAttemptValue - 1; + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(numberOfAttemptValue); + + List carsElements = Arrays.asList(new Car("name1"), new Car("name2")); + Cars cars = new Cars(carsElements); + + Round round = new Round(cars, numberOfAttempt); + + MovementStrategy movementStrategy = () -> true; + + //when + Round actual = round.play(movementStrategy); + Cars actualCars = actual.cars(); + List actualCarsElements = actualCars.elements(); + + //then + assertAll( + () -> assertThat(actualCarsElements).extracting("name.value", "position.value") + .containsExactly( + tuple("name1", 1), + tuple("name2", 1)), + () -> assertThat(actual).extracting("numberOfAttempt.value") + .isEqualTo(expectedNumberOfAttemptValue) + ); + } + + @ParameterizedTest + @CsvSource(value = {"0, false", "1, true"}) + @DisplayName("시도 횟수가 없으면 참 아니면 거짓을 반환한다") + void isPlaying(final int numberOfAttemptValue, final boolean expected) { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(numberOfAttemptValue); + + List elements = Arrays.asList(new Car("name1"), new Car("name2")); + Cars cars = new Cars(elements); + + Round round = new Round(cars, numberOfAttempt); + + //when + boolean actual = round.isPlaying(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("차들을 반환한다") + void cars() { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(1); + List elements = Arrays.asList(new Car("name1"), new Car("name2")); + Cars cars = new Cars(elements); + + Round round = new Round(cars, numberOfAttempt); + + //when + Cars actual = round.cars(); + + //then + assertThat(actual).isEqualTo(cars); + } + + @Test + @DisplayName("우승자를 반환한다") + void winners() { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(1); + Car winner1 = new Car("win1", 3); + Car winner2 = new Car("win2", 3); + Car loser = new Car("lose", 2); + List elements = Arrays.asList(winner1, winner2, loser); + Cars cars = new Cars(elements); + + Round round = new Round(cars, numberOfAttempt); + + //when + List winners = round.winners(); + + //then + assertThat(winners).extracting("name.value", "position.value") + .containsExactly( + tuple("win1", 3), + tuple("win2", 3) + ); + } +} diff --git a/src/test/java/racing/domain/vo/NameTest.java b/src/test/java/racing/domain/vo/NameTest.java new file mode 100644 index 000000000..03927e957 --- /dev/null +++ b/src/test/java/racing/domain/vo/NameTest.java @@ -0,0 +1,67 @@ +package racing.domain.vo; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class NameTest { + + @ParameterizedTest + @ValueSource(strings = {"c", "aaaaa"}) + @DisplayName("이름의 길이가 1이상 5이하인 경우 값을 반환한다") + void name(String value) { + //given + Name name = new Name(value); + + //when + String actual = name.value(); + + //then + assertThat(actual).isEqualTo(value); + } + + @ParameterizedTest + @EmptySource + @ValueSource(strings = "aaaaaa") + @DisplayName("이름의 길이가 0이하, 5초과일 경우 예외가 발생한다") + void create_throw_exception_with_invalid_name_length(String value) { + //then + assertThatIllegalArgumentException().isThrownBy(() -> new Name(value)) + .withMessage("이름이 길이는 1이상 5이하여야 합니다"); + } + + @ParameterizedTest + @CsvSource(value = {"name,name,true", "name,other,false"}) + @DisplayName("이름이 같으면 참을 다르면 거짓을 반환한다") + void equals(String value, String otherValue, boolean expected) { + //given + Name name = new Name(value); + Name other = new Name(otherValue); + + //when + boolean actual = name.equals(other); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"name,name,true", "name,other,false"}) + @DisplayName("이름이 같으면 같은 해시값을 반환하고, 다르면 다른 해시값을 반환한다") + void hashCode(String value, String otherValue, boolean expected) { + //given + Name name = new Name(value); + Name other = new Name(otherValue); + + //when + boolean actual = name.hashCode() == other.hashCode(); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/racing/domain/vo/NumberOfAttemptTest.java b/src/test/java/racing/domain/vo/NumberOfAttemptTest.java new file mode 100644 index 000000000..2fa4521bf --- /dev/null +++ b/src/test/java/racing/domain/vo/NumberOfAttemptTest.java @@ -0,0 +1,97 @@ +package racing.domain.vo; + +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 org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class NumberOfAttemptTest { + + @ParameterizedTest + @ValueSource(ints = {1}) + @DisplayName("시도 횟수를 반환한다") + void value(final int value) { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(value); + + //when + int actual = numberOfAttempt.value(); + + //then + assertThat(actual).isEqualTo(value); + } + + @Test + @DisplayName("시도 횟수가 0이하의 수라면 예외를 발생시킨다") + void create_throw_exception_with_zero_or_less_number() { + //given + int invalidValue = -1; + + //then + assertThatIllegalArgumentException().isThrownBy(() -> new NumberOfAttempt(invalidValue)) + .withMessageContaining("시도 횟수는 0 이상의 수 입니다"); + } + + @ParameterizedTest + @CsvSource(value = "2, 1") + @DisplayName("한 번 시도하면 시도 횟수가 1 차감된다") + void decrease(final int value, final int expectedValue) { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(value); + + //when + NumberOfAttempt actual = numberOfAttempt.decrease(); + int actualValue = actual.value(); + + //then + assertThat(actualValue).isEqualTo(expectedValue); + } + + @ParameterizedTest + @CsvSource(value = {"0, true", "1, false"}) + @DisplayName("시도 횟수가 0이면 참 0이상이면 거짓을 반환한다") + void isOver(final int value, final boolean expected) { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(value); + + //when + boolean actual = numberOfAttempt.isOver(); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,true", "1,0,false"}) + @DisplayName("") + void equals(final int value, final int otherValue, final boolean expected) { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(value); + NumberOfAttempt other = new NumberOfAttempt(otherValue); + + //when + boolean actual = numberOfAttempt.equals(other); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,true", "1,0,false"}) + @DisplayName("") + void hash(final int value, final int otherValue, final boolean expected) { + //given + NumberOfAttempt numberOfAttempt = new NumberOfAttempt(value); + NumberOfAttempt other = new NumberOfAttempt(otherValue); + + //when + boolean actual = numberOfAttempt.hashCode() == other.hashCode(); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/racing/domain/vo/PositionTest.java b/src/test/java/racing/domain/vo/PositionTest.java new file mode 100644 index 000000000..9641b2054 --- /dev/null +++ b/src/test/java/racing/domain/vo/PositionTest.java @@ -0,0 +1,68 @@ +package racing.domain.vo; + +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 static org.assertj.core.api.Assertions.assertThat; + +class PositionTest { + + @Test + @DisplayName("위치를 초기화 하면 위치값을 반환한다") + void create() { + //given + Position position = new Position(1); + + //when + int actual = position.value(); + + //then + assertThat(actual).isEqualTo(1); + } + + @Test + @DisplayName("증가할 위치 값을 인자로 받아 위치 값을 증가시킨다") + void increaseByOne() { + //given + Position position = new Position(0); + Position increasedPosition = position.increaseBy(1); + + //when + int actual = increasedPosition.value(); + + //then + assertThat(actual).isEqualTo(1); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,true", "1,2,false"}) + @DisplayName("위치값이 같으면 참, 다르면 거짓을 반환한다") + void equals(int value, int otherValue, boolean expected) { + //given + Position position = new Position(value); + Position other = new Position(otherValue); + + //when + boolean actual = position.equals(other); + + //then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"1,1,true", "1,2,false"}) + @DisplayName("위치값이 같으면 참, 다르면 거짓을 반환한다") + void hashCode(int value, int otherValue, boolean expected) { + //given + Position position = new Position(value); + Position other = new Position(otherValue); + + //when + boolean actual = position.hashCode() == other.hashCode(); + + //then + assertThat(actual).isEqualTo(expected); + } +} diff --git a/src/test/java/racing/dto/InputCarNameDtoTest.java b/src/test/java/racing/dto/InputCarNameDtoTest.java new file mode 100644 index 000000000..64f772b45 --- /dev/null +++ b/src/test/java/racing/dto/InputCarNameDtoTest.java @@ -0,0 +1,36 @@ +package racing.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.domain.Names; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class InputCarNameDtoTest { + + @Test + @DisplayName("차 이름들을 입력을 받아 차 이름 값 객체들을 반환한다") + void toNames() { + //given + String[] nameElements = new String[]{"name1", "name2", "name3"}; + InputCarNameDto inputCarNameDto = new InputCarNameDto(nameElements); + + //when + Names names = inputCarNameDto.toNames(); + + //then + assertThat(names).isNotNull(); + } + + @Test + @DisplayName("이름에 중복이 존재하면 예외를 발생시킨다") + void create_throw_exception_with_duplicated_name() { + //given + String[] nameElements = {"name", "name"}; + + //then + assertThatIllegalArgumentException().isThrownBy(() -> new InputCarNameDto(nameElements)) + .withMessage("중복된 이름이 존재합니다"); + } +} diff --git a/src/test/java/racing/dto/InputNumberOfAttemptDtoTest.java b/src/test/java/racing/dto/InputNumberOfAttemptDtoTest.java new file mode 100644 index 000000000..732b23de3 --- /dev/null +++ b/src/test/java/racing/dto/InputNumberOfAttemptDtoTest.java @@ -0,0 +1,37 @@ +package racing.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.domain.vo.NumberOfAttempt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class InputNumberOfAttemptDtoTest { + + @Test + @DisplayName("시도 횟수를 인자로 받아 값을 반환한다") + void getNumberOfAttempt() { + //given + int expected = 3; + InputNumberOfAttemptDto inputNumberOfAttemptDto = new InputNumberOfAttemptDto(expected); + + //when + NumberOfAttempt actual = inputNumberOfAttemptDto.toNumberOfAttempt(); + + //then + assertThat(actual).extracting("value") + .isEqualTo(expected); + } + + @Test + @DisplayName("시도 횟수가 숫자가 아닐 경우 예외를 발생시킨다") + void create_throw_exception_with_not_digit() { + //given + String invalidNumberOfAttempt = "String"; + + //then + assertThatIllegalArgumentException().isThrownBy(() -> new InputNumberOfAttemptDto(invalidNumberOfAttempt)) + .withMessageContaining("올바른 시도 횟수가 아닙니다"); + } +} diff --git a/src/test/java/racing/dto/OutputRoundResultDtoTest.java b/src/test/java/racing/dto/OutputRoundResultDtoTest.java new file mode 100644 index 000000000..c41476691 --- /dev/null +++ b/src/test/java/racing/dto/OutputRoundResultDtoTest.java @@ -0,0 +1,38 @@ +package racing.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.domain.Car; +import racing.domain.Cars; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +class OutputRoundResultDtoTest { + + @Test + @DisplayName("차 일급컬렉션을 입력받아 차 리스트를 반환한다") + void cars() { + //given + Car car1 = new Car("name1", 1); + Car car2 = new Car("name2", 2); + List elements = Arrays.asList(car1, car2); + + Cars cars = new Cars(elements); + + OutputRoundResultDto outputRoundResultDto = new OutputRoundResultDto(cars); + + //when + List actual = outputRoundResultDto.cars(); + + //then + assertThat(actual).extracting("name.value", "position.value") + .containsExactly( + tuple("name1", 1), + tuple("name2", 2) + ); + } +} diff --git a/src/test/java/racing/dto/OutputWinnerDtoTest.java b/src/test/java/racing/dto/OutputWinnerDtoTest.java new file mode 100644 index 000000000..943480c49 --- /dev/null +++ b/src/test/java/racing/dto/OutputWinnerDtoTest.java @@ -0,0 +1,27 @@ +package racing.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racing.domain.Car; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutputWinnerDtoTest { + + @Test + @DisplayName("우승자들을 이름으로 받아 우승자들의 이름을 반환한다") + void winners() { + //given + List winners = Arrays.asList(new Car("name1"), new Car("name2")); + OutputWinnerDto outputWinnerDto = new OutputWinnerDto(winners); + + //when + List actual = outputWinnerDto.winners(); + + //then + assertThat(actual).containsExactly("name1", "name2"); + } +} diff --git a/src/test/java/racing/strategy/RandomMovementStrategyTest.java b/src/test/java/racing/strategy/RandomMovementStrategyTest.java new file mode 100644 index 000000000..35b9efb71 --- /dev/null +++ b/src/test/java/racing/strategy/RandomMovementStrategyTest.java @@ -0,0 +1,27 @@ +package racing.strategy; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class RandomMovementStrategyTest { + + @ParameterizedTest + @CsvSource(value = {"true,true", "false,false"}) + void canMove(boolean randomValue, boolean expected) { + //given + MovementStrategy movementStrategy = new RandomMovementStrategyStrategy() { + @Override + protected boolean generateRandom() { + return randomValue; + } + }; + + //when + boolean actual = movementStrategy.canMove(); + + //then + assertThat(actual).isEqualTo(expected); + } +}