diff --git a/README.md b/README.md index d0286c859f..c38c0ddaa2 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# java-racingcar-precourse +# java-racingcar-precourse + +## 기능 목록 +- [X] 자동차 이름 입력 안내문구를 출력한다 +- [X] 경주할 자동차 이름을 입력 받는다 +- [X] 경주할 자동차를 생성한다 +- [X] 경주할 각 자동차 이름 길이를 검증한다 (5자 이하) +- [X] 시도 횟수 입력 안내문구를 출력한다 +- [X] 몇 번을 이동할 지 입력 받는다 +- [X] 무작위 값을 생성한다 +- [X] 무작위 값에 따라 하나의 자동차를 전진 혹은 정지시킨다 +- [X] 한 번의 이동시도를 수행한다. 입력받은 모든 자동차를 이동시도한다 +- [X] 모든 차의 상태를 출력한다 +- [X] 입력한 횟수 만큼의 이동시도 수행을 반복한다 +- [X] 이동시도 수행을 마치고 실행 결과를 출력한다 +- [X] 최종 우승자(들)을 판별한다 +- [X] 최종 우승자(들)을 출력한다 + +--- + +## 추가 예외 케이스, 개발 과정에서 수정 예정 +- [X] 시도 횟수를 검증한다 (숫자로 변환 가능한지, overflow 검증, 음수가 아닌지) +- [X] 경주할 자동차 이름 입력을 검증한다 + - [X] 자동차 이름 입력이 아예 없을 때를 검증한다 + - [X] 자동차의 각 이름이 비어있을 때를 검증한다 + - [X] 구분자 위치에 따른 다양한 예외 상황을 검증한다 diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e724..34eb33ad9d 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,12 @@ package racingcar; +import racingcar.config.ControllerConfig; + public class Application { + public static void main(String[] args) { - // TODO: 프로그램 구현 + ControllerConfig config = new ControllerConfig(); + config.controller().run(); } + } diff --git a/src/main/java/racingcar/argumentresolver/ArgumentResolver.java b/src/main/java/racingcar/argumentresolver/ArgumentResolver.java new file mode 100644 index 0000000000..56dc2d23a2 --- /dev/null +++ b/src/main/java/racingcar/argumentresolver/ArgumentResolver.java @@ -0,0 +1,7 @@ +package racingcar.argumentresolver; + +public interface ArgumentResolver { + + T parse(String parameter); + +} diff --git a/src/main/java/racingcar/argumentresolver/AttemptCountArgumentResolver.java b/src/main/java/racingcar/argumentresolver/AttemptCountArgumentResolver.java new file mode 100644 index 0000000000..dc9657f28c --- /dev/null +++ b/src/main/java/racingcar/argumentresolver/AttemptCountArgumentResolver.java @@ -0,0 +1,14 @@ +package racingcar.argumentresolver; + +public class AttemptCountArgumentResolver implements ArgumentResolver { + + @Override + public Integer parse(String parameter) { + try { + return Integer.parseInt(parameter); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("숫자로 변환가능한 입력이어야 합니다."); + } + } + +} diff --git a/src/main/java/racingcar/argumentresolver/CarArgumentResolver.java b/src/main/java/racingcar/argumentresolver/CarArgumentResolver.java new file mode 100644 index 0000000000..a887cd6937 --- /dev/null +++ b/src/main/java/racingcar/argumentresolver/CarArgumentResolver.java @@ -0,0 +1,28 @@ +package racingcar.argumentresolver; + +import java.util.Arrays; +import java.util.List; + +public class CarArgumentResolver implements ArgumentResolver> { + + private static final int NEGATIVE = -1; + + @Override + public List parse(String parameter) { + checkBlank(parameter); + return splitWithBlankAndTrim(parameter); + } + + private List splitWithBlankAndTrim(String parameter) { + return Arrays.stream(parameter.split(",", NEGATIVE)) + .map(String::trim) + .toList(); + } + + private void checkBlank(String parameter) { + if (parameter == null || parameter.isBlank()) { + throw new IllegalArgumentException("비어있는 입력은 불가능합니다."); + } + } + +} diff --git a/src/main/java/racingcar/config/ControllerConfig.java b/src/main/java/racingcar/config/ControllerConfig.java new file mode 100644 index 0000000000..4eed6843dd --- /dev/null +++ b/src/main/java/racingcar/config/ControllerConfig.java @@ -0,0 +1,85 @@ +package racingcar.config; + +import java.util.List; +import racingcar.domain.mover.AllCarMover; +import racingcar.domain.mover.CarMover; +import racingcar.controller.Controller; +import racingcar.view.InputView; +import racingcar.domain.winnerdecider.policy.MaxMoveWinningPolicy; +import racingcar.domain.mover.policy.MovePolicy; +import racingcar.view.OutputView; +import racingcar.domain.mover.policy.RandomMovePolicy; +import racingcar.domain.mover.policy.trigger.RandomNumberGenerator; +import racingcar.domain.winnerdecider.WinnersDecider; +import racingcar.domain.winnerdecider.policy.WinningPolicy; +import racingcar.argumentresolver.ArgumentResolver; +import racingcar.argumentresolver.AttemptCountArgumentResolver; +import racingcar.argumentresolver.CarArgumentResolver; +import racingcar.validator.RangeValidator; +import racingcar.validator.SizeValidator; +import racingcar.validator.Validator; + +public class ControllerConfig { + + public Controller controller() { + return new Controller( + outputView(), + inputView(), + carArgumentResolver(), + attemptCountArgumentResolver(), + sizeValidator(), + rangeValidator(), + allCarMover(), + winnersDecider() + ); + } + + public OutputView outputView() { + return new OutputView(); + } + + public InputView inputView() { + return new InputView(); + } + + public ArgumentResolver> carArgumentResolver() { + return new CarArgumentResolver(); + } + + public ArgumentResolver attemptCountArgumentResolver() { + return new AttemptCountArgumentResolver(); + } + + public Validator> sizeValidator() { + return new SizeValidator(); + } + + public Validator rangeValidator() { + return new RangeValidator(); + } + + public AllCarMover allCarMover() { + return new AllCarMover(carMover()); + } + + public CarMover carMover() { + return new CarMover(movePolicy()); + } + + public MovePolicy movePolicy() { + return new RandomMovePolicy(randomNumberGenerator()); + } + + public RandomNumberGenerator randomNumberGenerator() { + return new RandomNumberGenerator(); + } + + public WinnersDecider winnersDecider() { + return new WinnersDecider(winningPolicy()); + } + + public WinningPolicy winningPolicy() { + return new MaxMoveWinningPolicy(); + } + +} diff --git a/src/main/java/racingcar/controller/Controller.java b/src/main/java/racingcar/controller/Controller.java new file mode 100644 index 0000000000..f85a2668af --- /dev/null +++ b/src/main/java/racingcar/controller/Controller.java @@ -0,0 +1,89 @@ +package racingcar.controller; + +import java.util.List; +import racingcar.argumentresolver.ArgumentResolver; +import racingcar.domain.Car; +import racingcar.domain.mover.AllCarMover; +import racingcar.util.CarConverter; +import racingcar.domain.winnerdecider.WinnersDecider; +import racingcar.validator.Validator; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class Controller { + + private final OutputView outputView; + private final InputView inputView; + private final ArgumentResolver> carArgumentResolver; + private final ArgumentResolver attemptCountArgumentResolver; + private final Validator> sizeValidator; + private final Validator rangeValidator; + private final AllCarMover allCarMover; + private final WinnersDecider winnersDecider; + + public Controller( + OutputView outputView, + InputView inputView, + ArgumentResolver> carArgumentResolver, + ArgumentResolver attemptCountArgumentResolver, + Validator> sizeValidator, + Validator rangeValidator, + AllCarMover allCarMover, + WinnersDecider winnersDecider + ) { + this.outputView = outputView; + this.inputView = inputView; + this.carArgumentResolver = carArgumentResolver; + this.attemptCountArgumentResolver = attemptCountArgumentResolver; + this.sizeValidator = sizeValidator; + this.rangeValidator = rangeValidator; + this.allCarMover = allCarMover; + this.winnersDecider = winnersDecider; + } + + public void run() { + List cars = getCarsFromInput(); + int attemptCount = getAttemptCountFromInput(); + runRace(cars, attemptCount); + printWinners(cars); + } + + private List getCarsFromInput() { + outputView.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + String racingCarNamesRequest = inputView.read(); + + List carNames = carArgumentResolver.parse(racingCarNamesRequest); + sizeValidator.check(carNames); + + return CarConverter.toCars(carNames); + } + + private int getAttemptCountFromInput() { + outputView.println("시도할 횟수는 몇 회인가요?"); + String attemptCountRequest = inputView.read(); + + int attemptCount = attemptCountArgumentResolver.parse(attemptCountRequest); + rangeValidator.check(attemptCount); + + return attemptCount; + } + + private void runRace(List cars, int attemptCount) { + outputView.endLine(); + outputView.println("실행 결과"); + + for (int count = 0; count < attemptCount; count++) { + allCarMover.run(cars); + outputView.printCarsStatus(cars); + outputView.endLine(); + } + } + + private void printWinners(List cars) { + List winners = winnersDecider.run(cars); + List winnerNames = CarConverter.toNames(winners); + + outputView.print("최종 우승자 : "); + outputView.printNames(winnerNames); + } +} diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000000..7fddf25c4f --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,25 @@ +package racingcar.domain; + +public class Car { + + private final String name; + private int moveCount; + + + public Car(String name) { + this.name = name; + } + + public void move() { + moveCount++; + } + + public String getName() { + return name; + } + + public int getMoveCount() { + return moveCount; + } + +} diff --git a/src/main/java/racingcar/domain/mover/AllCarMover.java b/src/main/java/racingcar/domain/mover/AllCarMover.java new file mode 100644 index 0000000000..2b36449429 --- /dev/null +++ b/src/main/java/racingcar/domain/mover/AllCarMover.java @@ -0,0 +1,18 @@ +package racingcar.domain.mover; + +import java.util.List; +import racingcar.domain.Car; + +public class AllCarMover { + + private final CarMover carMover; + + public AllCarMover(CarMover carMover) { + this.carMover = carMover; + } + + public void run(List cars) { + cars.forEach(carMover::run); + } + +} diff --git a/src/main/java/racingcar/domain/mover/CarMover.java b/src/main/java/racingcar/domain/mover/CarMover.java new file mode 100644 index 0000000000..4555e951a4 --- /dev/null +++ b/src/main/java/racingcar/domain/mover/CarMover.java @@ -0,0 +1,20 @@ +package racingcar.domain.mover; + +import racingcar.domain.mover.policy.MovePolicy; +import racingcar.domain.Car; + +public class CarMover { + + private final MovePolicy movePolicy; + + public CarMover(MovePolicy movePolicy) { + this.movePolicy = movePolicy; + } + + public void run(final Car car) { + if (movePolicy.canMove()) { + car.move(); + } + } + +} diff --git a/src/main/java/racingcar/domain/mover/policy/MovePolicy.java b/src/main/java/racingcar/domain/mover/policy/MovePolicy.java new file mode 100644 index 0000000000..af7dd866d3 --- /dev/null +++ b/src/main/java/racingcar/domain/mover/policy/MovePolicy.java @@ -0,0 +1,7 @@ +package racingcar.domain.mover.policy; + +public interface MovePolicy { + + boolean canMove(); + +} diff --git a/src/main/java/racingcar/domain/mover/policy/RandomMovePolicy.java b/src/main/java/racingcar/domain/mover/policy/RandomMovePolicy.java new file mode 100644 index 0000000000..f908fadbeb --- /dev/null +++ b/src/main/java/racingcar/domain/mover/policy/RandomMovePolicy.java @@ -0,0 +1,20 @@ +package racingcar.domain.mover.policy; + +import racingcar.domain.mover.policy.trigger.RandomNumberGenerator; + +public class RandomMovePolicy implements MovePolicy { + + private static final int MOVE_THRESHOLD = 4; + private final RandomNumberGenerator randomNumberGenerator; + + public RandomMovePolicy(RandomNumberGenerator randomNumberGenerator) { + this.randomNumberGenerator = randomNumberGenerator; + } + + @Override + public boolean canMove() { + int randomValue = randomNumberGenerator.run(); + return randomValue >= MOVE_THRESHOLD; + } + +} diff --git a/src/main/java/racingcar/domain/mover/policy/trigger/RandomNumberGenerator.java b/src/main/java/racingcar/domain/mover/policy/trigger/RandomNumberGenerator.java new file mode 100644 index 0000000000..8919b19821 --- /dev/null +++ b/src/main/java/racingcar/domain/mover/policy/trigger/RandomNumberGenerator.java @@ -0,0 +1,11 @@ +package racingcar.domain.mover.policy.trigger; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomNumberGenerator { + + public int run() { + return Randoms.pickNumberInRange(0, 9); + } + +} diff --git a/src/main/java/racingcar/domain/winnerdecider/WinnersDecider.java b/src/main/java/racingcar/domain/winnerdecider/WinnersDecider.java new file mode 100644 index 0000000000..e90e7a53f0 --- /dev/null +++ b/src/main/java/racingcar/domain/winnerdecider/WinnersDecider.java @@ -0,0 +1,19 @@ +package racingcar.domain.winnerdecider; + +import java.util.List; +import racingcar.domain.Car; +import racingcar.domain.winnerdecider.policy.WinningPolicy; + +public class WinnersDecider { + + private final WinningPolicy winningPolicy; + + public WinnersDecider(WinningPolicy winningPolicy) { + this.winningPolicy = winningPolicy; + } + + public List run(final List cars) { + return winningPolicy.determineWinners(cars); + } + +} diff --git a/src/main/java/racingcar/domain/winnerdecider/policy/MaxMoveWinningPolicy.java b/src/main/java/racingcar/domain/winnerdecider/policy/MaxMoveWinningPolicy.java new file mode 100644 index 0000000000..d99331890a --- /dev/null +++ b/src/main/java/racingcar/domain/winnerdecider/policy/MaxMoveWinningPolicy.java @@ -0,0 +1,24 @@ +package racingcar.domain.winnerdecider.policy; + +import java.util.List; +import racingcar.domain.Car; + +public class MaxMoveWinningPolicy implements WinningPolicy { + + @Override + public List determineWinners(List cars) { + final int maxMoveCount = getMaxMoveCount(cars); + return cars.stream() + .filter(car -> car.getMoveCount() == maxMoveCount) + .toList(); + } + + private int getMaxMoveCount(final List cars) { + int maxMoveCount = 0; + for (Car car : cars) { + maxMoveCount = Math.max(maxMoveCount, car.getMoveCount()); + } + return maxMoveCount; + } + +} diff --git a/src/main/java/racingcar/domain/winnerdecider/policy/WinningPolicy.java b/src/main/java/racingcar/domain/winnerdecider/policy/WinningPolicy.java new file mode 100644 index 0000000000..7d4dbc0962 --- /dev/null +++ b/src/main/java/racingcar/domain/winnerdecider/policy/WinningPolicy.java @@ -0,0 +1,10 @@ +package racingcar.domain.winnerdecider.policy; + +import java.util.List; +import racingcar.domain.Car; + +public interface WinningPolicy { + + List determineWinners(List cars); + +} diff --git a/src/main/java/racingcar/util/CarConverter.java b/src/main/java/racingcar/util/CarConverter.java new file mode 100644 index 0000000000..e27508795d --- /dev/null +++ b/src/main/java/racingcar/util/CarConverter.java @@ -0,0 +1,23 @@ +package racingcar.util; + +import java.util.List; +import racingcar.domain.Car; + +public final class CarConverter { + + private CarConverter() { + } + + public static List toCars(List carNames) { + return carNames.stream() + .map(Car::new) + .toList(); + } + + public static List toNames(List cars) { + return cars.stream() + .map(Car::getName) + .toList(); + } + +} diff --git a/src/main/java/racingcar/validator/RangeValidator.java b/src/main/java/racingcar/validator/RangeValidator.java new file mode 100644 index 0000000000..d355f33617 --- /dev/null +++ b/src/main/java/racingcar/validator/RangeValidator.java @@ -0,0 +1,13 @@ +package racingcar.validator; + +public class RangeValidator implements Validator { + + private static final int MIN_ATTEMPTS_COUNT = 0; + + @Override + public void check(Integer data) { + if (data < MIN_ATTEMPTS_COUNT) { + throw new IllegalArgumentException(String.format("%d 이상의 숫자만 가능합니다.", MIN_ATTEMPTS_COUNT)); + } + } +} diff --git a/src/main/java/racingcar/validator/SizeValidator.java b/src/main/java/racingcar/validator/SizeValidator.java new file mode 100644 index 0000000000..d652733976 --- /dev/null +++ b/src/main/java/racingcar/validator/SizeValidator.java @@ -0,0 +1,25 @@ +package racingcar.validator; + +import java.util.List; + +public class SizeValidator implements Validator> { + + private static final Integer MIN_LENGTH = 0; + private static final Integer MAX_LENGTH = 5; + + @Override + public void check(List values) { + if (haveNotValidSizeValue(values)) { + throw new IllegalArgumentException(String.format("%d자 초과, %d자 이하만 가능합니다.", MIN_LENGTH, MAX_LENGTH)); + } + } + + private boolean haveNotValidSizeValue(List values) { + return values.stream() + .anyMatch(this::isNotValidSize); + } + + private boolean isNotValidSize(String value) { + return MIN_LENGTH >= value.length() || value.length() > MAX_LENGTH; + } +} diff --git a/src/main/java/racingcar/validator/Validator.java b/src/main/java/racingcar/validator/Validator.java new file mode 100644 index 0000000000..b0678a7564 --- /dev/null +++ b/src/main/java/racingcar/validator/Validator.java @@ -0,0 +1,7 @@ +package racingcar.validator; + +public interface Validator { + + void check(T data); + +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000000..a32a0d6928 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,14 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + + public String read() { + final String command = Console.readLine(); + // Console.close(); + + return command; + } + +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000000..549458f4c4 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,49 @@ +package racingcar.view; + +import java.util.List; +import racingcar.domain.Car; + +public class OutputView { + + public void print(final String message) { + System.out.print(message); + } + + public void endLine() { + System.out.println(); + } + + public void println(final String message) { + print(message); + endLine(); + } + + public void printCarsStatus(final List cars) { + cars.forEach(this::printCarStatus); + } + + public void printCarStatus(final Car car) { + String message = carNameFormatWhenShowStatus(car.getName()) + + carMoveCountFormatWhenShowStatus(car.getMoveCount()); + println(message); + } + + public void printNames(final List names) { + String message = String.join(", ", names); + println(message); + } + + private String carNameFormatWhenShowStatus(final String carName) { + String format = "%s : "; + return String.format(format, carName); + } + + private String carMoveCountFormatWhenShowStatus(final int moveCount) { + StringBuilder builder = new StringBuilder(); + for (int count = 0; count < moveCount; count++) { + builder.append("-"); + } + return builder.toString(); + } + +} diff --git a/src/test/java/racingcar/argumentresovler/AttemptCountArgumentResolverTest.java b/src/test/java/racingcar/argumentresovler/AttemptCountArgumentResolverTest.java new file mode 100644 index 0000000000..089dbdecc8 --- /dev/null +++ b/src/test/java/racingcar/argumentresovler/AttemptCountArgumentResolverTest.java @@ -0,0 +1,63 @@ +package racingcar.argumentresovler; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.argumentresolver.AttemptCountArgumentResolver; + +class AttemptCountArgumentResolverTest { + + private static final String ERROR_MESSAGE = "숫자로 변환가능한 입력이어야 합니다."; + + AttemptCountArgumentResolver argumentResolver = new AttemptCountArgumentResolver(); + + @Test + @DisplayName("숫자로 변환 불가능 입력이 들어오면 에러 발생") + void parse_숫자로_변환불가능한_입력_에러발생() { + // given + String noIntInput = "a"; + String emptyInput = ""; + String nullInput = null; + + // when & then + assertThatThrownBy(() -> argumentResolver.parse(noIntInput)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE); + + assertThatThrownBy(() -> argumentResolver.parse(emptyInput)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE); + + assertThatThrownBy(() -> argumentResolver.parse(nullInput)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE); + } + + @Test + @DisplayName("overflow 에러 발생") + void parse_overflow_입력_에러발생() { + // given + String overflowInput = "10000000000"; + + // when & then + assertThatThrownBy(() -> argumentResolver.parse(overflowInput)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE); + } + + @Test + @DisplayName("정상 입력이 들어오면 성공") + void parse_정상입력_성공() { + // given + String input = "5"; + + // when + int parsedInt = argumentResolver.parse(input); + + // then + assertThat(parsedInt).isEqualTo(5); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/argumentresovler/CarArgumentResolverTest.java b/src/test/java/racingcar/argumentresovler/CarArgumentResolverTest.java new file mode 100644 index 0000000000..563c444e2d --- /dev/null +++ b/src/test/java/racingcar/argumentresovler/CarArgumentResolverTest.java @@ -0,0 +1,92 @@ +package racingcar.argumentresovler; + +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; +import racingcar.argumentresolver.CarArgumentResolver; + +class CarArgumentResolverTest { + + private static final String ERROR_MESSAGE = "비어있는 입력은 불가능합니다."; + + CarArgumentResolver carArgumentResolver = new CarArgumentResolver(); + + @Test + @DisplayName("자동차 이름 String이 주어졌을 때 ,를 기준으로 정상 parse") + public void parse_테스트() { + // given + String rawCarNames = "a,b,c"; + + // when + List carNames = carArgumentResolver.parse(rawCarNames); + + // then + assertThat(carNames).contains("a"); + assertThat(carNames).contains("b"); + assertThat(carNames).contains("c"); + } + + @Test + @DisplayName("앞뒤로 공백이 있을 경우, 공백을 제거하고 정상 parse") + public void parse_trim_테스트() { + // given + String rawCarNames = " a , b , c "; + + // when + List carNames = carArgumentResolver.parse(rawCarNames); + + // then + assertThat(carNames).contains("a"); + assertThat(carNames).contains("b"); + assertThat(carNames).contains("c"); + } + + @Test + @DisplayName("구분자가 앞에 있을 경우 살려서 정상 parse") + public void parseSplitWithBlankFront_테스트() { + // given + String rawCarNames = ",b"; + + // when + List carNames = carArgumentResolver.parse(rawCarNames); + + // then + assertThat(carNames).contains(""); + assertThat(carNames).contains("b"); + } + + @Test + @DisplayName("구분자가 뒤에 있을 경우 살려서 정상 parse") + public void parseSplitWithBlankBack_테스트() { + // given + String rawCarNames = "b,"; + + // when + List carNames = carArgumentResolver.parse(rawCarNames); + + // then + assertThat(carNames).contains(""); + assertThat(carNames).contains("b"); + } + + @Test + @DisplayName("비어있거나 null 값이 입력으로 주어질 때 에러 발생") + void parse_비어있는_값이_입력으로_주어질_때() { + // given + String nullString = null; + String emptyString = ""; + + // when & then + assertThatThrownBy(() -> carArgumentResolver.parse(nullString)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE); + + assertThatThrownBy(() -> carArgumentResolver.parse(emptyString)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/CarNameIntegrationTest.java b/src/test/java/racingcar/domain/CarNameIntegrationTest.java new file mode 100644 index 0000000000..006ab4c540 --- /dev/null +++ b/src/test/java/racingcar/domain/CarNameIntegrationTest.java @@ -0,0 +1,70 @@ +package racingcar.domain; + +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 racingcar.argumentresolver.CarArgumentResolver; +import racingcar.validator.SizeValidator; + +public class CarNameIntegrationTest { + + private CarArgumentResolver argumentResolver = new CarArgumentResolver(); + private SizeValidator validator = new SizeValidator(); + + + static final Integer MIN_LENGTH = 0; + static final Integer MAX_LENGTH = 5; + static final String ERROR_MESSAGE_VALIDATOR = "%d자 초과, %d자 이하만 가능합니다."; + static final String ERROR_MESSAGE_RESOLVER = "비어있는 입력은 불가능합니다."; + + @Test + @DisplayName("비어있을 경우 argumentResolver 에 의해 에러 발생") + void 비어있는_입력_테스트() { + // given + String whiteSpaceInput = " "; + String emptyInput = ""; + String nullInput = null; + + // when & then + whenSplitToArgumentResolverThenException(whiteSpaceInput); + whenSplitToArgumentResolverThenException(emptyInput); + whenSplitToArgumentResolverThenException(nullInput); + } + + @Test + @DisplayName("구분자가 앞뒤로 있거나 연속해서 오면 validator 에 의해 에러 발생") + void 구분자_예외_테스트() { + // given + List errorInput = List.of( + ",", + "a,", + ",a", + ",a,", + "a,,b" + ); + + List> afterSplit = errorInput.stream() + .map((i) -> argumentResolver.parse(i)) + .toList(); + + // when & then + for (List names : afterSplit) { + whenCheckToValidatorAndThenException(names); + } + } + + private void whenSplitToArgumentResolverThenException(String input) { + assertThatThrownBy(() -> argumentResolver.parse(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ERROR_MESSAGE_RESOLVER); + } + + private void whenCheckToValidatorAndThenException(List rawCarNames) { + assertThatThrownBy(() -> validator.check(rawCarNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(ERROR_MESSAGE_VALIDATOR, MIN_LENGTH, MAX_LENGTH)); + } + +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java new file mode 100644 index 0000000000..6bef0de8b3 --- /dev/null +++ b/src/test/java/racingcar/domain/CarTest.java @@ -0,0 +1,43 @@ +package racingcar.domain; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; + +class CarTest { + + Car car; + String carName = "test"; + + @BeforeEach + void setUp() { + car = new Car(carName); + } + + @Test + @DisplayName("getName 호출로 차이름 조회 테스트") + void getName_차이름_조회테스트() { + // when & then + assertThat(car.getName()).isEqualTo(carName); + } + + @Test + @DisplayName("int_인스턴스변수는_객체생성시_값이없다면_0으로_할당된다") + void getMoveCount_초기값_테스트() { + // when & then + assertThat(car.getMoveCount()).isEqualTo(0); + } + + @Test + @DisplayName("1번 움직인 후 moveCount 조회") + void getMoveCount_실제_움직임_반영_테스트() { + // when + car.move(); + + // when & then + assertThat(car.getMoveCount()).isEqualTo(1); + } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/mover/AllCarMoverTest.java b/src/test/java/racingcar/domain/mover/AllCarMoverTest.java new file mode 100644 index 0000000000..58488dfadb --- /dev/null +++ b/src/test/java/racingcar/domain/mover/AllCarMoverTest.java @@ -0,0 +1,71 @@ +package racingcar.domain.mover; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.domain.mover.AllCarMover; +import racingcar.domain.mover.CarMover; +import racingcar.domain.mover.policy.MovePolicy; + +import static org.assertj.core.api.Assertions.assertThat; + +class AllCarMoverTest { + + private CarMover trueCarMover; + private CarMover falseCarMover; + private List cars; + + @BeforeEach + void setUp() { + cars = List.of( + new Car("a"), + new Car("b") + ); + + trueCarMover = new CarMover(new TrueMockMovePolicy()); + falseCarMover = new CarMover(new FalseMockMovePolicy()); + } + + @Test + @DisplayName("모든 차가 이동하는 경우 테스트") + void 모든_차_이동_테스트() { + // given + AllCarMover allCarMover = new AllCarMover(trueCarMover); + + // when + allCarMover.run(cars); + + // then + assertThat(cars).allMatch(car -> car.getMoveCount() == 1); + } + + @Test + @DisplayName("모든 차가 이동하지 않는 경우 테스트") + void 모든_차_정지_테스트() { + // given + AllCarMover allCarMover = new AllCarMover(falseCarMover); + + // when + allCarMover.run(cars); + + // then + assertThat(cars).allMatch(car -> car.getMoveCount() == 0); + } + + static class TrueMockMovePolicy implements MovePolicy { + @Override + public boolean canMove() { + return true; + } + } + + static class FalseMockMovePolicy implements MovePolicy { + @Override + public boolean canMove() { + return false; + } + } + +} diff --git a/src/test/java/racingcar/domain/mover/CarMoverTest.java b/src/test/java/racingcar/domain/mover/CarMoverTest.java new file mode 100644 index 0000000000..58a58bab5d --- /dev/null +++ b/src/test/java/racingcar/domain/mover/CarMoverTest.java @@ -0,0 +1,58 @@ +package racingcar.domain.mover; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.domain.mover.CarMover; +import racingcar.domain.mover.policy.MovePolicy; + +import static org.assertj.core.api.Assertions.assertThat; + +class CarMoverTest { + + private Car car; + + @BeforeEach + void setUp() { + car = new Car("test"); + } + + @Test + void 전진정책을_항상_true를_반환하는_Mock_객체를_주입_전진성공() { + // given + CarMover carMover = new CarMover(new TrueMockMovePolicy()); + + // when + carMover.run(car); + + // then + assertThat(car.getMoveCount()).isEqualTo(1); + } + + @Test + void 전진정책을_항상_false를_반환하는_Mock_객체를_주입_그대로정지() { + // given + CarMover carMover = new CarMover(new FalseMockMovePolicy()); + + // when + carMover.run(car); + + // then + assertThat(car.getMoveCount()).isEqualTo(0); + } + + static class TrueMockMovePolicy implements MovePolicy { + @Override + public boolean canMove() { + return true; + } + } + + static class FalseMockMovePolicy implements MovePolicy { + @Override + public boolean canMove() { + return false; + } + } + +} diff --git a/src/test/java/racingcar/domain/mover/policy/trigger/RandomNumberGeneratorTest.java b/src/test/java/racingcar/domain/mover/policy/trigger/RandomNumberGeneratorTest.java new file mode 100644 index 0000000000..3fc8f40ffc --- /dev/null +++ b/src/test/java/racingcar/domain/mover/policy/trigger/RandomNumberGeneratorTest.java @@ -0,0 +1,22 @@ +package racingcar.domain.mover.policy.trigger; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.mover.policy.trigger.RandomNumberGenerator; + +class RandomNumberGeneratorTest { + + RandomNumberGenerator generator = new RandomNumberGenerator(); + + @Test + @DisplayName("0부터 9까지 범위의 랜덤 숫자 생성 성공") + public void 랜덤숫자_생성_0부터9까지_범위_성공() { + // when + int actualResult = generator.run(); + + // then + Assertions.assertThat(actualResult).isBetween(0, 9); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/util/CarConverterTest.java b/src/test/java/racingcar/util/CarConverterTest.java new file mode 100644 index 0000000000..5a85f43a0c --- /dev/null +++ b/src/test/java/racingcar/util/CarConverterTest.java @@ -0,0 +1,29 @@ +package racingcar.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.util.CarConverter; + +class CarConverterTest { + + @Test + @DisplayName("List을 List로 변환 성공 테스트") + public void stringList_carList_convert_테스트() { + // given + List carNames = List.of("a", "b", "c"); + + // when + List cars = CarConverter.toCars(carNames); + + // then + assertThat(cars.size()).isEqualTo(3); + assertThat(cars.get(0).getName()).isEqualTo("a"); + assertThat(cars.get(1).getName()).isEqualTo("b"); + assertThat(cars.get(2).getName()).isEqualTo("c"); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/validator/RangeValidatorTest.java b/src/test/java/racingcar/validator/RangeValidatorTest.java new file mode 100644 index 0000000000..e7290fc918 --- /dev/null +++ b/src/test/java/racingcar/validator/RangeValidatorTest.java @@ -0,0 +1,41 @@ +package racingcar.validator; + +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 racingcar.validator.RangeValidator; + +class RangeValidatorTest { + + private static final String ERROR_MESSAGE_FORMAT = "%d 이상의 숫자만 가능합니다."; + private static final int MIN_ATTEMPTS_COUNT = 0; + + RangeValidator validator = new RangeValidator(); + + @Test + @DisplayName("0 미만 입력이 들어오면 에러 발생") + void check_0_미만_입력_에러발생() { + // given + int input = -1; + + // when & then + assertThatThrownBy(() -> validator.check(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(ERROR_MESSAGE_FORMAT, MIN_ATTEMPTS_COUNT)); + } + + @Test + @DisplayName("정상 입력이 들어오면 성공") + void check_정상입력_성공() { + // given + int input = 1; + + // when & then + assertThatCode(() -> validator.check(input)) + .doesNotThrowAnyException(); + } + + +} \ No newline at end of file diff --git a/src/test/java/racingcar/validator/SizeValidatorTest.java b/src/test/java/racingcar/validator/SizeValidatorTest.java new file mode 100644 index 0000000000..cc66f2bec6 --- /dev/null +++ b/src/test/java/racingcar/validator/SizeValidatorTest.java @@ -0,0 +1,55 @@ +package racingcar.validator; + + +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 racingcar.validator.SizeValidator; + +class SizeValidatorTest { + + static final Integer MIN_LENGTH = 0; + static final Integer MAX_LENGTH = 5; + static final String ERROR_MESSAGE = "%d자 초과, %d자 이하만 가능합니다."; + + SizeValidator sizeValidator = new SizeValidator(); + + @Test + @DisplayName("5글자 초과시 IllegalArgumentException 발생") + void 글자길이_초과시_에러발생() { + // given + List rawCarNames = List.of("sizeOver"); + + // when & then + whenAndThen(rawCarNames); + } + + @Test + @DisplayName("비어있는 이름은 IllegalArgumentException 발생") + void 비어있는_이름_에러발생() { + // given + List rawCarNames = List.of(""); + + // when & then + whenAndThen(rawCarNames); + } + + @Test + @DisplayName("비어있는 이름이 한개라도 포함시 IllegalArgumentException 발생") + void 비어있는_이름이_한개라도_포함시_에러발생() { + // given + List rawCarNames = List.of("", "a"); + + // when & then + whenAndThen(rawCarNames); + } + + private void whenAndThen(List rawCarNames) { + assertThatThrownBy(() -> sizeValidator.check(rawCarNames)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(String.format(ERROR_MESSAGE, MIN_LENGTH, MAX_LENGTH)); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/view/InputViewTest.java b/src/test/java/racingcar/view/InputViewTest.java new file mode 100644 index 0000000000..a2e5c084f5 --- /dev/null +++ b/src/test/java/racingcar/view/InputViewTest.java @@ -0,0 +1,29 @@ +package racingcar.view; + + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.view.InputView; + +class InputViewTest { + + InputView inputView = new InputView(); + + @Test + @DisplayName("read 메서드 입력 테스트") + public void read_입력_테스트() { + // given + String command = "pobi,woni,jun"; + + InputStream in = new ByteArrayInputStream(command.getBytes()); + System.setIn(in); + + // when & then + Assertions.assertThat(inputView.read()) + .isEqualTo(command); + } + +} \ No newline at end of file diff --git a/src/test/java/racingcar/view/OutputViewTest.java b/src/test/java/racingcar/view/OutputViewTest.java new file mode 100644 index 0000000000..b126bf74d2 --- /dev/null +++ b/src/test/java/racingcar/view/OutputViewTest.java @@ -0,0 +1,74 @@ +package racingcar.view; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.domain.Car; +import racingcar.view.OutputView; + +class OutputViewTest { + + OutputView outputView; + OutputStream outputStream; + + + Car pobiCar = new Car("pobi"); + Car woniCar = new Car("woni"); + + String pobiMessage = "pobi : "; + String woniMessage = "woni : "; + String newLine = "\n"; + + @BeforeEach + void setUp() { + outputView = new OutputView(); + + outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + } + + @Test + @DisplayName("메세지 출력 테스트") + public void 메세지_출력_테스트() { + // given + String message = "Hello, world!"; + + // when + outputView.println(message); + + // then + assertThat(outputStream.toString()).isEqualTo(message + newLine); + } + + @Test + @DisplayName("printCarStatus 테스트") + public void printCarStatus_테스트() { + // when + outputView.printCarStatus(pobiCar); + + // then + assertThat(outputStream.toString()).isEqualTo(pobiMessage + newLine); + } + + @Test + @DisplayName("printCarsStatus 테스트") + public void printCarsStatus_테스트() { + // given + List cars = List.of(pobiCar, woniCar); + + // when + outputView.printCarsStatus(cars); + + // then + assertThat(outputStream.toString()) + .isEqualTo(pobiMessage + newLine + woniMessage + newLine); + } + + +} \ No newline at end of file diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java new file mode 100644 index 0000000000..dd3f543616 --- /dev/null +++ b/src/test/java/study/StringTest.java @@ -0,0 +1,63 @@ +package study; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +public class StringTest { + + String emptyString = ""; + String whiteSpaceString = " "; + String nullString = null; + String delim = ","; + + @Test + void isBlankTest() { + // when & then + assertThat(emptyString.isBlank()).isTrue(); + assertThat(whiteSpaceString.isBlank()).isTrue(); + assertThatThrownBy(() -> nullString.isBlank()) + .isInstanceOf(NullPointerException.class); + } + + @Test + void isEmptyTest() { + // when & then + assertThat(emptyString.isEmpty()).isTrue(); + assertThat(whiteSpaceString.isEmpty()).isFalse(); + assertThatThrownBy(() -> nullString.isBlank()) + .isInstanceOf(NullPointerException.class); + } + + @Test + void splitTest() { + // given + String a = ""; + String b = ","; + String c = "a,"; + String d = ",a"; + String e = "a,,a"; + + // when + debug(a); + debug(b); + debug(c); + debug(d); + debug(e); + } + + private void debug(String input) { + print(input.split(delim)); + print(input.split(delim, -1)); + System.out.println(); + } + + private void print(String[] split) { + System.out.println("split.length = " + split.length); + for (String s : split) { + System.out.println("s = " + s); + } + } + +}