diff --git a/QUESTION.md b/QUESTION.md new file mode 100644 index 00000000..032385ed --- /dev/null +++ b/QUESTION.md @@ -0,0 +1,29 @@ +# 질문 모음 + ++ 입력값 검증에 대한 테스트 코드를 짤 때, 잘못된 입력값 여러 개를 테스트 하고 싶다면 주로 어떻게 하는 것이 좋은지? + +아래처럼 배열로 하는 것이 좋은건지, 아니면 다른 더 좋은 방법이 있는건지?! +```java +class RacerTest { + @Test + @DisplayName("Racer 생성자 실패 테스트") + void racerConstructorWithInvalidDataTest() { + // given: 생성자 데이터 + List invalidNameList = Arrays.asList(null, "", " "); + + for (String invalidName : invalidNameList) { + // when + ThrowableAssert.ThrowingCallable constructorCall = () -> new Racer(invalidName); + + // then + assertThatThrownBy(constructorCall) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(Racer.VALIDATE_NAME_ERROR_MESSAGE); + } + } +} +``` + ++ static 키워드에 대한 테스트에 대해서 강사님은 어떻게 생각하시는지? ++ entity, controller, view 여러 곳에 퍼져있는 validate 로직을 통합하는게 나은지, 아니면 지금대로 분산시켜서 각각 검증 로직을 들고 있는게 나은건지? ++ Test case, Test Fixture 등을 쉽게 만들어주는 라이브러리가 있는지? 아님 방법론이라던가?! \ No newline at end of file diff --git a/README.md b/README.md index 491aece1..c8780c0c 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -# java-racingcar-precourse \ No newline at end of file +# java-racingcar-precourse + +# 기능 목록 + +## Model + +### Racer Entity + +#### Data +- [x] 자동차 경주자는 이름이 있다. + - [x] NAME_MAX_LENGTH 이하의 길이여야 한다. +- [x] 자동차 경주자는 이동한 거리를 정수로 기록한다. + +#### Act +- [x] 자동차 경주자는 전진할 수 있다. + - [x] 전진할 때 숫자로 된 입력을 받는다. + - [x] 입력받은 숫자가 threshold 값을 넘어야 전진한다. + - [x] threshold 값을 넘지 못하면 전진하지 않는다. +- [ ] ~~자동차 경주자는 이름과 함께 이동한 거리를 출력할 수 있다.~~ +- [x] 자동차 경주자는 숫자로 된 입력을 받아, 우승했는지 여부를 알려준다. + +### Racer VO + +#### Data +- [x] 자동차 경주자는 이름이 있다. +- [x] 자동차 경주자는 이동한 거리를 정수로 기록한다. +- [x] 자동차 경주자는 우승 여부를 기록한다. + +### Controller + +#### Data +- [x] 자동차 경주자 목록을 가지고 있는다. +- [x] 자동차 경주 진행 시도 횟수를 가지고 있는다. +- [x] 현재 자동차 경주 진행 횟수를 가지고 있는다. + +#### Act +- [x] 자동차 이름을 1개의 문자열로 입력받아 자동차 목록을 생성한다. + - [x] 자동차 이름은 쉼표를 기준으로 구분하며, 이름이 NAME_MAX_LENGTH보다 길 경우 IllegalArgumentException을 발생시킨다. +- [x] 경주 게임을 진행할 횟수를 입력 받는다. + - [x] 횟수에는 최댓값 제한은 없으며, 음수 또는 숫자가 아닐 시 IllegalArgumentException을 발생시킨다. + - [x] 입력 받은 숫자로 경주 진행 시도 횟수를 초기화하고, 현재 자동차 경주 진행 횟수는 0으로 리셋한다. +- [x] 경주 게임을 1번 진행한다. + - [x] 만약 자동차 목록 또는 경주 게임 진행 횟수가 세팅이 안 되었을 경우 IllegalStateException을 발생시킨다. + - [x] 이미 게임이 종료 되었을 경우 IllegalStateException을 발생시킨다. + - [x] 게임이 끝나면 Racer VO 목록과 게임 종료 여부를 반환한다. + +### View + +#### Data +- [x] 자동차 경주 컨트롤러를 가지고 있는다. + +#### Act +- [x] 자동차 경주 게임을 시작한다. + - [x] 경주할 자동차 이름을 입력받는다. + - [x] 시도할 횟수를 입력 받는다. + - [x] 자동차 경주 게임을 진행하며 매번 결과를 출력한다. + - [x] 최종 우승자를 출력한다. + - [x] 입력을 받을 때 에러가 발생하면 "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다. + +### Util + +### RandomNumberGenerator + +#### Act +- [x] 입력 받은 숫자 범위에서 무작위 정수를 생성한다. + +### ~~ErrorMessageGenerator~~ + +#### ~~Act~~ +- [ ] ~~입력 받은 에러 메세지를 정해진 포맷으로 바꿔서 반환한다.~~ + +# 후기 + +기능 단위를 먼저 쭉 정리하고 개발을 시작하니까 좀 더 수월하게 할 수 있었습니다. 고민도 미리 하고 어느정도 설계가 나온 상태여서 그런 것 같습니다. 물론 진행 과정에서 여러모로 수정이 있었지만, 크게 수정된 것은 없었던 것 같습니다. + + +RandomNumberGenerator를 Mocking하는 과정에서, mocking은 static 키워드와 바이트 코드 수준에서의 선언 순서로 인한 문제점이 있다는 것을 처음 알게 되었습니다. 여러 해결 방법을 찾아보고 직접 적용해볼 수 있어서 흥미로웠습니다. 그동안 별 생각 없이 static 키워드와 함께 Util 클래스를 자주 사용하고, 테스트에서도 mockStatic를 사용했습니다. 근데 테스팅 과정에서 static은 bad pattern 이라는 의견도 많이 보여서 스스로 static이 포함된 테스트 코드의 작성 난이도를 고민해보게 되었습니다. + + +Generator를 싱글톤 또는 static 메소드로 쓰고는 싶은데, static 키워드 때문에 테스팅이 어려워져서 고민이 좀 많아졌습니다. Spring을 쓰면 Util class를 Bean으로 등록해서 싱글톤으로 쓰면 되지만, 이번 프로그램에서는 DI Container 같은 것 없이 직접 의존성을 해결해줘야 하다 보니까 더 고민이 많았네요. + + +고민거리가 많았던 토이 프로젝트라 재밌었습니다ㅎㅎ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 20a92c9e..55680f59 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ repositories { dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.assertj:assertj-core:3.25.3' + testImplementation 'org.mockito:mockito-core:5.12.0' } test { diff --git a/src/main/java/RacerApplication.java b/src/main/java/RacerApplication.java new file mode 100644 index 00000000..fe0452ff --- /dev/null +++ b/src/main/java/RacerApplication.java @@ -0,0 +1,12 @@ +import controller.RacerController; +import view.RacerView; + +public class RacerApplication { + public static void main(String[] args) { + RacerView racerView = new RacerView(new RacerController()); + + while (true) { + racerView.render(); + } + } +} diff --git a/src/main/java/controller/RacerController.java b/src/main/java/controller/RacerController.java new file mode 100644 index 00000000..efa6e9ff --- /dev/null +++ b/src/main/java/controller/RacerController.java @@ -0,0 +1,117 @@ +package controller; + +import dto.RacerDto; +import dto.RacerResult; +import entity.Racer; +import utils.RandomNumberGenerator; + +import java.math.BigInteger; +import java.util.*; + +public class RacerController { + public static final String VALIDATE_GAME_COUNT_ERROR_MESSAGE = "gameCount는 null이거나 음수일 수 없습니다."; + + public static final String VALIDATE_RACER_LIST_ERROR_MESSAGE = "racerList는 null이거나 빈 배열일 수 없습니다."; + + public static final String VALIDATE_GAME_ENDED_ERROR_MESSAGE = "이미 종료된 게임입니다."; + + private List racerList; + + private BigInteger maxGameCount; + + private BigInteger currentGameCount; + + public RacerController() { + this.racerList = new ArrayList<>(); + this.maxGameCount = new BigInteger("-1"); + this.currentGameCount = new BigInteger("-1"); + } + + /** + * @throws IllegalArgumentException nameString이 null 또는 빈 문자열일 때, ","를 기준으로 split 했을 때 빈 문자열인 경우 + */ + public void setUp(List nameList, BigInteger input) { + validateGameCount(input); + + List newRacerList = nameList.stream() + .map(Racer::new) + .toList(); + + maxGameCount = input; + currentGameCount = BigInteger.ZERO; + + racerList.clear(); + racerList.addAll(newRacerList); + } + + public RacerResult playGame() { + validatePlayGame(); + + currentGameCount = currentGameCount.add(BigInteger.ONE); + + for (Racer racer : racerList) { + int randomInteger = RandomNumberGenerator.getInstance().getRandomNumber(0, 9); + racer.moveIfCan(randomInteger); + } + + return new RacerResult( + isEnded(), + racerList.stream().map(this::getRacerDto).toList() + ); + } + + public boolean isEnded() { + return maxGameCount.equals(currentGameCount); + } + + private RacerDto getRacerDto(Racer racer) { + if (isEnded()) { + return RacerDto.of(racer, getMax()); + } + + return new RacerDto( + racer.getName(), + racer.getMovedDistance(), + false + ); + } + + private BigInteger getMax() { + if (isEnded()) { + return racerList.stream() + .map(Racer::getMovedDistance) + .max(BigInteger::compareTo) + .orElseThrow(() -> new IllegalStateException(VALIDATE_RACER_LIST_ERROR_MESSAGE)); + } + + return null; + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException(Racer.VALIDATE_NAME_ERROR_MESSAGE); + } + } + + private void validateRacerList() { + if (racerList == null || racerList.isEmpty()) { + throw new IllegalStateException(VALIDATE_RACER_LIST_ERROR_MESSAGE); + } + } + + private void validateGameCount(BigInteger integer) { + if (integer == null || integer.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException(VALIDATE_GAME_COUNT_ERROR_MESSAGE); + } + } + + private void validatePlayGame() { + validateRacerList(); + validateGameCount(maxGameCount); + validateGameCount(currentGameCount); + + if (isEnded()) { + throw new IllegalStateException(VALIDATE_GAME_ENDED_ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/dto/RacerDto.java b/src/main/java/dto/RacerDto.java new file mode 100644 index 00000000..cf872e2a --- /dev/null +++ b/src/main/java/dto/RacerDto.java @@ -0,0 +1,21 @@ +package dto; + +import entity.Racer; + +import java.math.BigInteger; + +public record RacerDto( + String name, + + BigInteger movedDistance, + + boolean isWinner +) { + public static RacerDto of(Racer racer, BigInteger input) { + return new RacerDto( + racer.getName(), + racer.getMovedDistance(), + racer.isWinner(input) + ); + } +} diff --git a/src/main/java/dto/RacerResult.java b/src/main/java/dto/RacerResult.java new file mode 100644 index 00000000..c08a9be3 --- /dev/null +++ b/src/main/java/dto/RacerResult.java @@ -0,0 +1,10 @@ +package dto; + +import java.util.Collection; + +public record RacerResult ( + boolean isEnded, + + Collection racerDtos +) { +} diff --git a/src/main/java/entity/Racer.java b/src/main/java/entity/Racer.java new file mode 100644 index 00000000..4d21abec --- /dev/null +++ b/src/main/java/entity/Racer.java @@ -0,0 +1,49 @@ +package entity; + +import java.math.BigInteger; + +public class Racer { + public static final int NAME_MAX_LENGTH = 5; + + public static final int MOVE_THRESHOLD = 3; + + public static final String VALIDATE_NAME_ERROR_MESSAGE = "name은 null이거나 빈 문자열일 수 없습니다. 그리고 " + NAME_MAX_LENGTH + "자 이하 여야합니다."; + + private String name; + + private BigInteger movedDistance; + + /** + * @throws IllegalArgumentException name이 null이거나 blank 문자열 일 때 + */ + public Racer(String name) { + this.name = validateName(name); + this.movedDistance = new BigInteger("0"); + } + + public void moveIfCan(int input) { + if(input > MOVE_THRESHOLD) { + movedDistance = movedDistance.add(new BigInteger("1")); + } + } + + public boolean isWinner(BigInteger input) { + return input.compareTo(movedDistance) <= 0; + } + + public String getName() { + return name; + } + + public BigInteger getMovedDistance() { + return movedDistance; + } + + private String validateName(String name) { + if (name == null || name.isBlank() || name.trim().length() > NAME_MAX_LENGTH) { + throw new IllegalArgumentException(VALIDATE_NAME_ERROR_MESSAGE); + } + + return name.trim(); + } +} diff --git a/src/main/java/utils/RandomNumberGenerator.java b/src/main/java/utils/RandomNumberGenerator.java new file mode 100644 index 00000000..e4cf23b9 --- /dev/null +++ b/src/main/java/utils/RandomNumberGenerator.java @@ -0,0 +1,28 @@ +package utils; + +import java.util.Random; + +public class RandomNumberGenerator { + private RandomNumberGenerator() { + + } + + public static RandomNumberGenerator getInstance() { + return LazyHolder.INSTANCE; + } + + public int getRandomNumber(int start, int end) { + if (start > end) { + int temp = start; + start = end; + end = temp; + } + + return LazyHolder.random.nextInt((end - start) + 1) + start; + } + + private static class LazyHolder { + private static final Random random = new Random(); + private static final RandomNumberGenerator INSTANCE = new RandomNumberGenerator(); + } +} diff --git a/src/main/java/view/RacerView.java b/src/main/java/view/RacerView.java new file mode 100644 index 00000000..fbaa9060 --- /dev/null +++ b/src/main/java/view/RacerView.java @@ -0,0 +1,130 @@ +package view; + +import controller.RacerController; +import dto.RacerDto; +import dto.RacerResult; +import entity.Racer; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class RacerView { + private final RacerController racerController; + + private final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); + + private List nameList = null; + + private BigInteger tryCount = null; + + private RacerResult result = null; + + public RacerView(RacerController racerController) { + this.racerController = racerController; + } + + public void render() { + + if (nameList == null) { + try { + String input = getInput(); + + nameList = Arrays.stream(input.split(",")) + .map(this::validateName) + .toList(); + } catch (IllegalArgumentException e) { + System.out.println(getFormattedErrorMessage(e.getMessage())); + return; + } + } + + if (tryCount == null) { + try { + tryCount = getTryCount(); + } catch (IllegalArgumentException e) { + System.out.println(getFormattedErrorMessage(e.getMessage())); + return; + } + } + + racerController.setUp(nameList, tryCount); + + System.out.println("출력 결과"); + + while (!racerController.isEnded()) { + result = racerController.playGame(); + + for (RacerDto racerDto : result.racerDtos()) { + String distanceString = getDistanceString(racerDto); + + System.out.println(racerDto.name() + " : " + distanceString); + } + + System.out.print("\n"); + } + + if (result != null) { + String winners = result.racerDtos().stream() + .filter(RacerDto::isWinner) + .map(RacerDto::name) + .collect(Collectors.joining(", ")); + System.out.println("최종 우승자 : " + winners); + } + + nameList = null; + tryCount = null; + result = null; + } + + private String getDistanceString(RacerDto racerDto) { + BigInteger movedDistance = racerDto.movedDistance(); + StringBuilder distanceStrBuilder = new StringBuilder(); + + // BigInteger를 사용하여 반복문 구현 + for (BigInteger i = BigInteger.ZERO; i.compareTo(movedDistance) < 0; i = i.add(BigInteger.ONE)) { + distanceStrBuilder.append('-'); + } + + return distanceStrBuilder.toString(); + } + + private String getInput() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + try { + return bufferedReader.readLine(); + } catch (Exception e) { + System.out.println(getFormattedErrorMessage("입력을 처리할 수 없습니다.")); + return null; + } + } + + private BigInteger getTryCount() { + System.out.println("시도할 회수는 몇회인가요?"); + + try { + return new BigInteger(bufferedReader.readLine().trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("숫자만 입력 가능합니다."); + } catch (Exception e) { + System.out.println(getFormattedErrorMessage("입력을 처리할 수 없습니다.")); + return null; + } + } + + private String getFormattedErrorMessage(String errorMessage) { + return "[ERROR] " + errorMessage; + } + + private String validateName(String name) { + if (name == null || name.isBlank() || name.trim().length() > Racer.NAME_MAX_LENGTH) { + throw new IllegalArgumentException(Racer.VALIDATE_NAME_ERROR_MESSAGE); + } + + return name.trim(); + } +} diff --git a/src/test/java/controller/RacerControllerTest.java b/src/test/java/controller/RacerControllerTest.java new file mode 100644 index 00000000..a128faf8 --- /dev/null +++ b/src/test/java/controller/RacerControllerTest.java @@ -0,0 +1,169 @@ +package controller; + +import dto.RacerDto; +import dto.RacerResult; +import entity.Racer; +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import utils.RandomNumberGenerator; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +class RacerControllerTest { + private RandomNumberGenerator mockRNG = Mockito.mock(RandomNumberGenerator.class); + + @Test + @DisplayName("Controller setUp 메소드 성공 테스트") + void setUpTest() { + // given + RacerController controller = new RacerController(); + BigInteger givenInput = new BigInteger("0"); + + // when + ThrowableAssert.ThrowingCallable setUpName = () -> controller.setUp(getValidNameInputString(), givenInput); + + // then + assertThatCode(setUpName).doesNotThrowAnyException(); + } + + @Test + @DisplayName("Controller setUp 메소드 숫자로 인한 실패 테스트") + void setUp_WillThrownByIntegerTest() { + // given + RacerController controller = new RacerController(); + BigInteger givenInput = new BigInteger("-1"); + + // when + ThrowableAssert.ThrowingCallable setUp = () -> controller.setUp(getValidNameInputString(), givenInput); + + // then + assertThatThrownBy(setUp) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(RacerController.VALIDATE_GAME_COUNT_ERROR_MESSAGE); + } + + @Test + @DisplayName("Controller playGame 메소드 성공 테스트") + void playGameTest() { + // given + RandomNumberGenerator generator = mock(RandomNumberGenerator.class); + RacerController controller = new RacerController(); + List racerDtoList = List.of(new RacerDto( + getValidNameInputString().get(0), + BigInteger.ZERO, + false + ) + ); + + controller.setUp(getValidNameInputString(), new BigInteger("2")); + + try (MockedStatic generatorMockedStatic = mockStatic(RandomNumberGenerator.class)) { + given(RandomNumberGenerator.getInstance()).willReturn(generator); + + when(generator.getRandomNumber(anyInt(), anyInt())) + .thenReturn(Racer.MOVE_THRESHOLD); + + // when + RacerResult result = controller.playGame(); + + // then + assertThat(result.isEnded()).isEqualTo(false); + assertThat(result.racerDtos()).isEqualTo(racerDtoList); + } + } + + @Test + @DisplayName("Controller playGame 메소드에서 게임 종료 테스트") + void playGameTest_WillEndedTest() { + // given + RandomNumberGenerator generator = mock(RandomNumberGenerator.class); + RacerController controller = new RacerController(); + List racerDtoList = List.of(new RacerDto( + getValidNameInputString().get(0), + BigInteger.ONE, + true + ) + ); + + controller.setUp(getValidNameInputString(), new BigInteger("1")); + + try (MockedStatic generatorMockedStatic = mockStatic(RandomNumberGenerator.class)) { + given(RandomNumberGenerator.getInstance()).willReturn(generator); + + when(generator.getRandomNumber(anyInt(), anyInt())) + .thenReturn(Racer.MOVE_THRESHOLD + 1); + + // when + RacerResult result = controller.playGame(); + + // then + assertThat(result.isEnded()).isEqualTo(true); + assertThat(result.racerDtos()).isEqualTo(racerDtoList); + } + } + + @Test + @DisplayName("Controller setUp 없이 playGame 메소드 실행하면 실패하는 테스트") + void playGameTest_ThrowsExceptionWhenNotSetUp_Test() { + // given + RandomNumberGenerator generator = mock(RandomNumberGenerator.class); + RacerController controller = new RacerController(); + + try (MockedStatic generatorMockedStatic = mockStatic(RandomNumberGenerator.class)) { + given(RandomNumberGenerator.getInstance()).willReturn(generator); + + when(generator.getRandomNumber(anyInt(), anyInt())) + .thenReturn(Racer.MOVE_THRESHOLD + 1); + + // when + ThrowableAssert.ThrowingCallable throwingCallable = controller::playGame; + + // then + assertThatThrownBy(throwingCallable) + .isInstanceOf(IllegalStateException.class) + .hasMessage(RacerController.VALIDATE_RACER_LIST_ERROR_MESSAGE); + } + } + + @Test + @DisplayName("Controller playGame 메소드에서 게임 종료 이후 게임을 진행하면 실패하는 테스트") + void playGameTest_ThrowsExceptionWhenEnded_Test() { + // given + RandomNumberGenerator generator = mock(RandomNumberGenerator.class); + RacerController controller = new RacerController(); + + controller.setUp(getValidNameInputString(), new BigInteger("1")); + + try (MockedStatic generatorMockedStatic = mockStatic(RandomNumberGenerator.class)) { + given(RandomNumberGenerator.getInstance()).willReturn(generator); + + when(generator.getRandomNumber(anyInt(), anyInt())) + .thenReturn(Racer.MOVE_THRESHOLD + 1); + + controller.playGame(); + + // when + ThrowableAssert.ThrowingCallable throwingCallable = controller::playGame; + + // then + assertThatThrownBy(throwingCallable) + .isInstanceOf(IllegalStateException.class) + .hasMessage(RacerController.VALIDATE_GAME_ENDED_ERROR_MESSAGE); + } + } + + private List getValidNameInputString() { + return List.of("pobi"); + } +} diff --git a/src/test/java/dto/RacerDtoTest.java b/src/test/java/dto/RacerDtoTest.java new file mode 100644 index 00000000..f75541d5 --- /dev/null +++ b/src/test/java/dto/RacerDtoTest.java @@ -0,0 +1,31 @@ +package dto; + +import entity.Racer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.assertj.core.api.Assertions.*; + +class RacerDtoTest { + @Test + @DisplayName("RacerDto of 메소드 테스트") + void ofTest() { + // given + Racer givenRacer = new Racer(getValidName()); + BigInteger givenInput = givenRacer.getMovedDistance();; + + // when + RacerDto racerDto = RacerDto.of(givenRacer, givenInput); + + // then + assertThat(racerDto.name()).isEqualTo(givenRacer.getName()); + assertThat(racerDto.movedDistance()).isEqualTo(givenRacer.getMovedDistance()); + assertThat(racerDto.isWinner()).isEqualTo(givenRacer.isWinner(givenInput)); + } + + private String getValidName() { + return "Test"; + } +} diff --git a/src/test/java/entity/RacerTest.java b/src/test/java/entity/RacerTest.java new file mode 100644 index 00000000..ca23d38f --- /dev/null +++ b/src/test/java/entity/RacerTest.java @@ -0,0 +1,103 @@ +package entity; + +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class RacerTest { + @Test + @DisplayName("Racer 생성자 테스트") + void racerConstructorTest() { + // given: 생성자 데이터 + String givenName = "Racer "; + String expectedName = "Racer"; + BigInteger expectedMovedDistance = new BigInteger("0"); + + // when + Racer racer = new Racer(givenName); + + // then + assertThat(racer.getName()).isEqualTo(expectedName); + assertThat(racer.getMovedDistance()).isEqualTo(expectedMovedDistance); + } + + @Test + @DisplayName("Racer 생성자 실패 테스트") + void racerConstructorWithInvalidDataTest() { + // given: 생성자 데이터 + List invalidNameList = Arrays.asList(null, "", " ", "5자이상문자"); + + for (String invalidName : invalidNameList) { + // when + ThrowableAssert.ThrowingCallable constructorCall = () -> new Racer(invalidName); + + // then + assertThatThrownBy(constructorCall) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(Racer.VALIDATE_NAME_ERROR_MESSAGE); + } + } + + @Test + @DisplayName("Racer moveIfCan 전진 테스트") + void racerMoveIfCan_WillMoveTest() { + // given: 전진하는 데이터 + int input = Racer.MOVE_THRESHOLD + 1; + Racer racer = new Racer(getValidName()); + + // when + racer.moveIfCan(input); + + // then + assertThat(racer.getMovedDistance()).isEqualTo(new BigInteger("1")); + } + + @Test + @DisplayName("Racer moveIfCan 정지 테스트") + void racerMoveIfCan_WillStopTest() { + // given: 전진하지 않는 데이터 + int input = Racer.MOVE_THRESHOLD; + Racer racer = new Racer(getValidName()); + + // when + racer.moveIfCan(input); + + // then + assertThat(racer.getMovedDistance()).isEqualTo(new BigInteger("0")); + } + + @Test + @DisplayName("Racer isWinner 테스트") + void racerIsWinnerTest_WillTrueTest() { + // given + Racer racer = new Racer(getValidName()); + BigInteger input = racer.getMovedDistance(); + boolean expectedResult = true; + + // when & then + assertThat(racer.isWinner(input)).isEqualTo(expectedResult); + } + + @Test + @DisplayName("Racer isWinner 테스트") + void racerIsWinnerTest_WillFalseTest() { + // given + Racer racer = new Racer(getValidName()); + BigInteger input = racer.getMovedDistance().add(new BigInteger("1")); + boolean expectedResult = false; + + // when & then + assertThat(racer.isWinner(input)).isEqualTo(expectedResult); + } + + private String getValidName() { + return "Test"; + } +}