diff --git a/README.md b/README.md index 8d7e8aee..c7dcce43 100644 --- a/README.md +++ b/README.md @@ -1 +1,51 @@ -# java-baseball-precourse \ No newline at end of file +# java-baseball-precourse + +크게 게임 초기화(컴퓨터), 사용자 입력, 게임 로직(판정), 게임 종료 및 재시작, 예외처리 5가지로 기능을 나누었습니다. + +1. 게임 초기화 (컴퓨터 숫자 생성) +2. 사용자 입력 +3. 게임 판정 및 힌트 출력 +4. 게임 종료 및 재시작 +5. 예외 처리 및 유효성 검사 + +## 기능 설명 +### 1. 게임 초기화 (컴퓨터 숫자 생성) +게임이 시작되면 컴퓨터는 아래의 규칙에 따라 숫자를 생성해야합니다. +- [x] 랜덤 수 생성: 1부터 9까지의 서로 다른 수로 이루어진 3자리 수를 임의로 선택합니다. + - [x] 각 자리의 숫자가 중복될 수 없다. + - [x] 0 은 포함되지 않는다. + +### 2. 사용자 입력 +사용자로부터 3자리의 숫자를 입력받는 기능입니다. +- [ ] 입력 요청: `숫자를 입력 해주세요`와 같은 문구로 사용자의 입력을 유도합니다. +- [ ] 입력값 수신: 사용자가 입력한 문자열(숫자)를 받아옵니다. + +### 3. 게임 판정 및 힌트 출력 +컴퓨터의 수 플레이어의 수를 비교하여 결과를 계산하고 출력합니다. + +- [x] 비교 로직 + - [x] `스트라이크`: 같은 수가 같은 자리에 있는 경우 + - [x] `볼`: 같은 수가 다른 자리에 있는 경우 + - [x] `낫싱`: 같은 수가 전혀 없는 경우 +- [x] 결과 출력 + - [x] 계산된 볼과 스트라이크 개수를 출력합니다. + - [x] 하나도 맞지 않을 경우 `낫싱`을 출력합니다. + - [x] 스트라이크와 볼이 같이 있는 경우, 보통 볼을 먼저 출력하고 스트라이크를 나중에 출력하는 형식을 따릅니다. + +### 4. 게임 종료 및 재시작 +3개의 숫자를 모두 맞혔을 때의 처리 로직입니다. +- [ ] 정답 처리: 3스트라이크일 경우, `3개의 숫자를 모두 맞히셨습니다! 게임종료` 메세지를 출력합니다. +- [ ] 재시작/종료 선택: 게임 종료 후 재시작 여부를 묻습니다. +- [ ] `1` 입력 시: 게임을 처음부터 다시 시작. 새로운 랜덤 숫자 생성 +- [ ] `2` 입력 시: 애플리케이션 완전히 종료 + +### 5. 예외 처리 및 유효성 검사 +사용자가 잘못된 값을 입력했을 때 프로그램이 종료되지 않고 알림을 준 뒤 다시 진행되어야 합니다. +- [x] 숫자가 아닌 문자가 포함된 경우. +- [x] 3자리가 아닌 경우 (2자리 이하 또는 4자리 이상) +- [x] 중복된 숫자가 있는 경우 (예: 112) +- [x] 1~9 범위를 벗어난 숫자(0)가 포함된 경우 + +에러처리는 `[ERROR]`로 시작하는 에러 메시지를 출력합니다. + +에러 발생 후 프로그램이 강제 종료되지 않아야 하고, 다시 `플레이어 입력 기능`으로 돌아가야합니다. \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..c4fa534e --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,18 @@ +import controller.GameController; +import domain.RandomNumberGenerator; +import domain.Referee; +import view.ConsoleInputView; +import view.ConsoleOutputView; + +public class Application { + public static void main(String[] args) { + GameController gameController = new GameController( + new ConsoleInputView(), + new ConsoleOutputView(), + new RandomNumberGenerator(), + new Referee() + ); + + gameController.run(); + } +} diff --git a/src/main/java/constant/ErrorMessage.java b/src/main/java/constant/ErrorMessage.java new file mode 100644 index 00000000..92563593 --- /dev/null +++ b/src/main/java/constant/ErrorMessage.java @@ -0,0 +1,11 @@ +package constant; + +public class ErrorMessage { + public static final String ERROR_PREFIX = "[ERROR] "; + public static final String NOT_NUMBER = ERROR_PREFIX + "숫자만 입력해주세요"; + public static final String OUT_OF_RANGE = ERROR_PREFIX + "숫자는 1부터 9까지의 수여야 합니다."; + public static final String DUPLICATE_NUMBER = ERROR_PREFIX + "숫자는 중복될 수 없습니다."; + public static final String INVALID_SIZE = ERROR_PREFIX + "숫자는 3자리여야 합니다."; + + private ErrorMessage() { } // 인스턴스 생성 방지 +} \ No newline at end of file diff --git a/src/main/java/constant/GameConfig.java b/src/main/java/constant/GameConfig.java new file mode 100644 index 00000000..157ae887 --- /dev/null +++ b/src/main/java/constant/GameConfig.java @@ -0,0 +1,8 @@ +package constant; + +public class GameConfig { + + public static final int NUMBERS_SIZE = 3; + public static final int NUMBER_MIN_RANGE = 1; + public static final int NUMBER_MAX_RANGE = 9; +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000..70752de3 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,70 @@ +package controller; + +import domain.NumberGenerator; +import dto.GameResult; +import domain.BaseballNumber; +import utils.InputConverter; +import domain.Referee; +import view.InputView; +import view.OutputView; + + +public class GameController { + + private final InputView inputView; + private final OutputView outputView; + private final NumberGenerator randomNumberGenerator; + private final Referee referee; + + public GameController( + InputView inputView, + OutputView outputView, + NumberGenerator randomNumberGenerator, + Referee referee + ) { + this.inputView = inputView; + this.outputView = outputView; + this.randomNumberGenerator = randomNumberGenerator; + this.referee = referee; + } + + public void run() { + outputView.printMessage("숫자 야구 게임을 시작하겠습니다."); + // 게임 전체 반복 (재시작 로직) + while (true) { + // 1. 컴퓨터 숫자 생성 + BaseballNumber computerNumber = randomNumberGenerator.generate(); + + // 2. 한 판 플레이 (3스트라이크 맞출 때까지 반복) + play(computerNumber); + + // 3. 게임 종료 후 재시작 여부 확인 + // "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요." + String restartCommand = inputView.inputRestartCommand(); + + if (restartCommand.equals("2")) { + break; // while문 탈출 -> 프로그램 종료 + } + // 1번이면 while문 처음으로 돌아가서 새 숫자 생성 + } + } + + private void play(BaseballNumber computerNumber) { + while(true) { + try { + String inputString = inputView.inputNumbers(); + BaseballNumber userNumber = new BaseballNumber(InputConverter.convertStringToIntegerList(inputString)); + GameResult result = referee.judge(computerNumber, userNumber); + outputView.printResult(result); + + if (result.isThreeStrike()) { + outputView.printGameSuccess(); + break; + } + } catch (IllegalArgumentException e) { + outputView.printMessage(e.getMessage()); + } + } + } + +} diff --git a/src/main/java/domain/BaseballNumber.java b/src/main/java/domain/BaseballNumber.java new file mode 100644 index 00000000..a2e8cd45 --- /dev/null +++ b/src/main/java/domain/BaseballNumber.java @@ -0,0 +1,66 @@ +package domain; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static constant.ErrorMessage.*; +import static constant.GameConfig.*; + +/** + * 숫자 3개를 담고 있는 객체 + * Computer, Referee, View 등의 객체에서 사용됩니다. + */ +public class BaseballNumber { + // 생성한 숫자라는 것은 숫자를 픽한것 뿐만 아니라 순서 정보도 가지고 있어야한다. + private final List numbers; + + public BaseballNumber(List numbers) { + validate(numbers); + this.numbers = numbers; + } + + private void validate(List numbers) { + validateSize(numbers); + validateRange(numbers); + validateDuplicate(numbers); + } + + private void validateSize(List numbers) { + if (numbers.size() != NUMBERS_SIZE) { + throw new IllegalArgumentException(INVALID_SIZE); + } + } + + private void validateRange(List numbers) { + for (int number: numbers) { + if (number < NUMBER_MIN_RANGE || number > NUMBER_MAX_RANGE) { + throw new IllegalArgumentException(OUT_OF_RANGE); + } + } + } + + private void validateDuplicate(List numbers) { + Set nonDuplicateNumbers = new HashSet<>(numbers); + if (nonDuplicateNumbers.size() != NUMBERS_SIZE) { + throw new IllegalArgumentException(DUPLICATE_NUMBER); + } + } + + // 유틸리티 메서드들 + + //특정 위치의 숫자를 가져오기 + public int getNumber(int index) { + return numbers.get(index); + } + + // 특정 숫자가 포함되어 있는지 확인 + public boolean contain(int number) { + return numbers.contains(number); + } + + public List getNumbers() { + return Collections.unmodifiableList(numbers); + } +} diff --git a/src/main/java/domain/NumberGenerator.java b/src/main/java/domain/NumberGenerator.java new file mode 100644 index 00000000..4d01ebc2 --- /dev/null +++ b/src/main/java/domain/NumberGenerator.java @@ -0,0 +1,6 @@ +package domain; + +public interface NumberGenerator { + + public BaseballNumber generate(); +} diff --git a/src/main/java/domain/RandomNumberGenerator.java b/src/main/java/domain/RandomNumberGenerator.java new file mode 100644 index 00000000..fd15a27b --- /dev/null +++ b/src/main/java/domain/RandomNumberGenerator.java @@ -0,0 +1,21 @@ +package domain; + +import java.util.*; + +public class RandomNumberGenerator implements NumberGenerator { + + private final Random random; + + public RandomNumberGenerator() { + this.random = new Random(); + } + + public BaseballNumber generate() { + Set uniqueNumbers = new LinkedHashSet<>(); + while (uniqueNumbers.size() < 3) { + uniqueNumbers.add(random.nextInt(9) + 1); + } + + return new BaseballNumber(new ArrayList<>(uniqueNumbers)); + } +} diff --git a/src/main/java/domain/Referee.java b/src/main/java/domain/Referee.java new file mode 100644 index 00000000..b5361a2b --- /dev/null +++ b/src/main/java/domain/Referee.java @@ -0,0 +1,27 @@ +package domain; + +import constant.GameConfig; +import dto.GameResult; + +public class Referee { + + public GameResult judge(BaseballNumber computer, BaseballNumber user) { + int balls = 0; + int strikes = 0; + + for (int i = 0; i < GameConfig.NUMBERS_SIZE; i++) { + int userNumber = user.getNumber(i); + + // 스트라이크인지 확인 (위치와 숫자가 모두 같음); + if (computer.getNumber(i) == userNumber) { + strikes++; + continue; // 볼 확인은 건너뜀 + } + if (computer.contain(userNumber)) { + balls++; + } + } + + return new GameResult(balls, strikes); + } +} diff --git a/src/main/java/dto/GameResult.java b/src/main/java/dto/GameResult.java new file mode 100644 index 00000000..5f0966fe --- /dev/null +++ b/src/main/java/dto/GameResult.java @@ -0,0 +1,38 @@ +package dto; + +public class GameResult { + private final int ball; + private final int strike; + + public GameResult(int ball, int strike) { + this.ball = ball; + this.strike = strike; + } + + public boolean isNothing() { + return ball == 0 && strike == 0; + } + + public boolean isThreeStrike() { + return strike == 3; + } + + public int getBall() { + return ball; + } + + public int getStrike() { + return strike; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (isThreeStrike()) { + sb.append("3스트라이크"); + } else { + sb.append(getBall() + "볼 " + getStrike() + "스트라이크"); + } + return sb.toString(); + } +} diff --git a/src/main/java/utils/InputConverter.java b/src/main/java/utils/InputConverter.java new file mode 100644 index 00000000..1c109213 --- /dev/null +++ b/src/main/java/utils/InputConverter.java @@ -0,0 +1,54 @@ +package utils; + +import constant.ErrorMessage; + +import java.util.ArrayList; +import java.util.List; + +import static constant.ErrorMessage.*; + +public class InputConverter { + + private InputConverter() {} + + public static List convertStringToIntegerList(String input) { + // null 및 공백처리 + if (input == null || input.isBlank()) { + throw new IllegalArgumentException(NOT_NUMBER); + } + + String trimmedInput = input.trim(); + + // 길이검증 + validateLength(trimmedInput); + List numbers = new ArrayList<>(); + + for (char character: trimmedInput.toCharArray()) { + validateDigit(character); + int number = Character.getNumericValue(character); + validateRange(number); + numbers.add(number); + } + return numbers; + } + + private static void validateLength(String input) { + if (input.length() != 3) { + throw new IllegalArgumentException(INVALID_SIZE); + } + } + + private static void validateDigit(char number) { + if (!Character.isDigit(number)) { + throw new IllegalArgumentException(OUT_OF_RANGE); + } + } + + private static void validateRange(int number) { + if (number == 0) { + throw new IllegalArgumentException(OUT_OF_RANGE); + } + } + + +} diff --git a/src/main/java/view/ConsoleInputView.java b/src/main/java/view/ConsoleInputView.java new file mode 100644 index 00000000..83115d6c --- /dev/null +++ b/src/main/java/view/ConsoleInputView.java @@ -0,0 +1,26 @@ +package view; + +import java.util.Scanner; + +public class ConsoleInputView implements InputView { + private static final String INPUT_MESSAGE = "숫자를 입력해 주세요 : "; + private static final String RESTART_MESSAGE = "게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요."; + private final Scanner scanner; + + public ConsoleInputView() { + this.scanner = new Scanner(System.in); + } + + public String inputNumbers() { + System.out.println(INPUT_MESSAGE); + + String input = scanner.nextLine(); + + return input.trim(); + } + + public String inputRestartCommand() { + System.out.println(RESTART_MESSAGE); // 안내 문구 출력 + return scanner.nextLine().trim(); // 입력받은 값(1 또는 2) 반환 + } +} diff --git a/src/main/java/view/ConsoleOutputView.java b/src/main/java/view/ConsoleOutputView.java new file mode 100644 index 00000000..b27bb0cb --- /dev/null +++ b/src/main/java/view/ConsoleOutputView.java @@ -0,0 +1,31 @@ +package view; + +import dto.GameResult; + +public class ConsoleOutputView implements OutputView { + + public void printResult(GameResult result) { + if (result.isNothing()) { + System.out.println("낫싱"); + return; + } + + StringBuilder sb = new StringBuilder(); + if (result.getBall() > 0) { + sb.append(result.getBall()).append("볼 "); + } + if (result.getStrike() > 0) { + sb.append(result.getStrike()).append("스트라이크 "); + } + + System.out.println(sb.toString().trim()); + } + + public void printGameSuccess() { + System.out.println("3개의 숫자를 모두 맞추셨습니다! 게임 종료"); + } + + public void printMessage(String message) { + System.out.println(message); + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..0701b59e --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,7 @@ +package view; + +public interface InputView { + + public String inputNumbers(); + public String inputRestartCommand(); +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..44e246a3 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,10 @@ +package view; + +import dto.GameResult; + +public interface OutputView { + + public void printResult(GameResult result); + public void printGameSuccess(); + public void printMessage(String message); +} diff --git a/src/test/java/controller/GameControllerTest.java b/src/test/java/controller/GameControllerTest.java new file mode 100644 index 00000000..e08f82b1 --- /dev/null +++ b/src/test/java/controller/GameControllerTest.java @@ -0,0 +1,157 @@ +package controller; + +import domain.BaseballNumber; +import domain.NumberGenerator; +import domain.Referee; +import dto.GameResult; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import view.InputView; +import view.OutputView; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +import static org.assertj.core.api.Assertions.*; + +class GameControllerTest { + + @Test + @DisplayName("게임 정상 실행: 오답 -> 정답 -> 종료(2) 시나리오 검증") + void gameRunSuccessScenario() { + // 컴퓨터의 입력을 1,2,3 으로 고정 + FixedNumberGenerator fixedGenerator = new FixedNumberGenerator("123"); + + // 사용자의 입력을 순서대로 설정 + StubInputView stubInputView = new StubInputView(List.of("145", "123"), "2"); + + // 출력을 캡쳐할 뷰 생성 + SpyOutputView spyOutputView = new SpyOutputView(); + + // 실제 심판 객체 + Referee referee = new Referee(); + + // 컨트롤러 조립 + GameController controller = new GameController( + stubInputView, + spyOutputView, + fixedGenerator, + referee + ); + + //when + controller.run(); + + // 출력된 메세지들이 예상대로 포함되어 있는지 + List logs = spyOutputView.getLogs(); + + assertThat(logs) + .containsExactly( + "숫자 야구 게임을 시작하겠습니다.", + "0볼 1스트라이크", // 145 입력 결과 + "3스트라이크", // 123 입력 결과 + "3개의 숫자를 모두 맞히셨습니다! 게임 종료" // 정답 메세지(OutputView 구현에 따라 다를 수 있음 + ); + } + + @Test + @DisplayName("예외 처리: 잘못된 입력 -> 에러 출력 -> 재입력 -> 성공 시나리오") + void gameRunExceptionScenario() { + FixedNumberGenerator fixedGenerator = new FixedNumberGenerator("123"); + // "abc" (에러발생) -> "123" (정답) -> "2" (종료) + StubInputView stubInputView = new StubInputView(List.of("abc", "123"), "2"); + SpyOutputView spyOutputView = new SpyOutputView(); + Referee referee = new Referee(); + + GameController controller = new GameController(stubInputView, spyOutputView, fixedGenerator, referee); + + //when + controller.run(); + + //then + List logs = spyOutputView.getLogs(); + assertThat(logs.toString()).contains("[ERROR]"); // 에러메세지가 출력되었는지 확인 + assertThat(logs).contains("3스트라이크"); // 결국 성공했는지 확인 + } + + /** + * 테스트를 위한 가짜 객체 정의 + * 무조건 정해진 숫자만 반환하는 가짜 생성기 + */ + static class FixedNumberGenerator implements NumberGenerator { + + private final List numbers; + + public FixedNumberGenerator(String input) { + this.numbers = new ArrayList<>(); + for (String s: input.split("")) { + numbers.add(Integer.parseInt(s)); + } + } + + @Override + public BaseballNumber generate() { + return new BaseballNumber(numbers); + } + } + + /** + * 미리 정해진 입력을 반환하는 테스트용 객체 + */ + static class StubInputView implements InputView { + + private final Queue numberInputs; + private final Queue restartInputs; + + public StubInputView( + List numberInputs, + String restartInputs + ) { + this.numberInputs = new LinkedList<>(numberInputs); + this.restartInputs = new LinkedList<>(List.of(restartInputs)); + } + + @Override + public String inputNumbers() { + return numberInputs.poll(); // 큐에서 하나씩 꺼냄 + } + + @Override + public String inputRestartCommand() { + return restartInputs.poll(); + } + } + + /** + * 출력된 내용을 리스트에 저장하는 가짜 객체 + */ + static class SpyOutputView implements OutputView { + + private final List logs = new ArrayList<>(); + + public List getLogs() { + return logs; + } + + @Override + public void printResult(GameResult result) { + // 실제 로직을 흉내내거나, 결과 객체 정보를 로그에 저장 + // 여기서는 간단히 문자로 변환해 저장한다고 가정 + String message = result.toString(); + logs.add(message); + } + + @Override + public void printGameSuccess() { + logs.add("3개의 숫자를 모두 맞히셨습니다! 게임 종료"); + } + + @Override + public void printMessage(String message) { + logs.add(message); + } + } +} \ No newline at end of file diff --git a/src/test/java/domain/BaseballNumberTest.java b/src/test/java/domain/BaseballNumberTest.java new file mode 100644 index 00000000..299b253a --- /dev/null +++ b/src/test/java/domain/BaseballNumberTest.java @@ -0,0 +1,50 @@ +package domain; + +import constant.ErrorMessage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class BaseballNumberTest { + + // BaseballNumber 객체를 생성하면서 검증로직도 함께 수행되기 때문에 + // 정상적으로 생성되는지, 검증로직이 제대로 동작하는지 테스트하면 좋을 것 같습니다. + + @Test + @DisplayName("성공: 정상적으로 객체를 생성") + void success() { + BaseballNumber numbers = new BaseballNumber(List.of(1, 2, 3)); + assertThat(numbers.getNumbers()) + .hasSize(3) + .containsExactly(1, 2, 3); + } + + // 정상적인 사이즈가 아닌경우, 중복된 원소가 있는경우, 1~9 사이의 범위를 벗어난 경우 예외가 발생합니다. + @Test + @DisplayName("예외: 사이즈가 4개인 야구번호 생성 시도") + void failWrongSize() { + assertThatThrownBy(() -> new BaseballNumber(List.of(1, 2, 3, 4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.INVALID_SIZE); + } + + @Test + @DisplayName("예외: 중복된 원소로 야구번호 생성 시도") + void failDuplicateNumber() { + assertThatThrownBy(() -> new BaseballNumber(List.of(1, 1, 2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.DUPLICATE_NUMBER); + } + + @Test + @DisplayName("예외: 1~9 이외의 범위로 야구번호 생성 시도") + void failWrongRange() { + assertThatThrownBy(() -> new BaseballNumber(List.of(0, 1, 2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.OUT_OF_RANGE); + } + +} \ No newline at end of file diff --git a/src/test/java/domain/RandomNumberGeneratorTest.java b/src/test/java/domain/RandomNumberGeneratorTest.java new file mode 100644 index 00000000..c159b003 --- /dev/null +++ b/src/test/java/domain/RandomNumberGeneratorTest.java @@ -0,0 +1,24 @@ +package domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class RandomNumberGeneratorTest { + + // 3개의 숫자가 담긴 BaseballNumber를 반환해주는지만 간단하게 테스트하면 좋을 것 같습니다. + @Test + @DisplayName("랜덤 숫자 3개 생성") + void testGenerateRandomThreeNumber() { + RandomNumberGenerator generator = new RandomNumberGenerator(); + BaseballNumber numbers = generator.generate(); + + assertThat(numbers.getNumbers()) + .isNotNull() + .hasSize(3); + } + +} \ No newline at end of file diff --git a/src/test/java/domain/RefereeTest.java b/src/test/java/domain/RefereeTest.java new file mode 100644 index 00000000..da0264c5 --- /dev/null +++ b/src/test/java/domain/RefereeTest.java @@ -0,0 +1,57 @@ +package domain; + +import dto.GameResult; +import org.assertj.core.api.Assertions; +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 java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class RefereeTest { + // judge 메서드를 테스트 해야함. + // 반환되는 GameResult 인스턴스에서 결과를 추출하고, + // 올바른 볼, 스트라이크 결과를 넘겨주는지에 대해 중점적으로 테스트한다. + // 잘못된 입력이 주어지는 예외 케이스에 대해서는 InputConverter에서 검사하기 때문에 따로 테스트하지 않는다. + + // 입력값과 기댓값만 바뀌고 검증로직은 똑같기 때문에 파라미터화 테스트가 적합하다고 생각했습니다. + + @DisplayName("다양한 경기 결과 판정 테스트") + @ParameterizedTest(name = "컴퓨터:{0}, 유저:{1} -> {2}볼 {3}스트라이크") + @CsvSource({ + "123, 123, 0, 3", // 3 스트라이크 + "123, 124, 0, 2", // 2 스트라이크 + "123, 456, 0, 0", // 낫싱 (0볼 0스트라이크) + "123, 312, 3, 0", // 3 볼 + "123, 132, 2, 1", // 2볼 1스트라이크 + "123, 415, 1, 0" // 1볼 + }) + void judge_parameterized_test(String computerStr, String userStr, int expectedBall, int expectedStrike) { + // given + BaseballNumber computer = new BaseballNumber(toList(computerStr)); + BaseballNumber user = new BaseballNumber(toList(userStr)); + Referee referee = new Referee(); + + // when + GameResult result = referee.judge(computer, user); + + // then + // 볼과 스트라이크 개수를 동시에 검증 + assertThat(result) + .extracting("ball", "strike") // 필드명 혹은 getter 이름 + .containsExactly(expectedBall, expectedStrike); + } + + private List toList(String input) { + List numbers = new ArrayList<>(); + for (String s : input.split("")) { + numbers.add(Integer.parseInt(s)); + } + return numbers; + } +} \ No newline at end of file diff --git a/src/test/java/utils/InputConverterTest.java b/src/test/java/utils/InputConverterTest.java new file mode 100644 index 00000000..1f49f146 --- /dev/null +++ b/src/test/java/utils/InputConverterTest.java @@ -0,0 +1,91 @@ +package utils; + +import constant.ErrorMessage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class InputConverterTest { + + @Test + @DisplayName("정상 입력: 3자리 숫자가 입력되면 리스트 [1,2,3]을 반환한다.") + void convertSuccess() { + // given + String input = "123"; + + // when + List result = InputConverter.convertStringToIntegerList(input); + + // then + assertThat(result) + .hasSize(3) + .containsExactly(1, 2, 3); + } + + @Test + @DisplayName("예외 발생: 입력값의 길이가 3자리가 아니면(2자리) 예외가 발생한다.") + void convertFailShortLength() { + // given + String input = "12"; + + // when & then + assertThatThrownBy(() -> InputConverter.convertStringToIntegerList(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.INVALID_SIZE); + } + + @Test + @DisplayName("예외 발생: 입력값의 길이가 3자리가 아니면(4자리) 예외가 발생한다.") + void convertFailLongLength() { + // given + String input = "1234"; + + // when & then + assertThatThrownBy(() -> InputConverter.convertStringToIntegerList(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.INVALID_SIZE); + } + + @Test + @DisplayName("예외 발생: 0이 포함되면 예외가 발생한다.") + void convertFailContainsZero() { + // given + String input = "012"; + + // when & then + assertThatThrownBy(() -> InputConverter.convertStringToIntegerList(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.OUT_OF_RANGE); + } + + @Test + @DisplayName("예외 발생: 숫자가 아닌 문자가 포함되면 예외가 발생한다.") + void convertFailNonNumeric() { + // given + String input = "1a3"; + + // when & then + assertThatThrownBy(() -> InputConverter.convertStringToIntegerList(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(ErrorMessage.OUT_OF_RANGE); + + } + + @Test + @DisplayName("엣지 케이스: 앞뒤 공백은 제거하고 숫자만 3개라면 통과한다.") + void convertSuccessWithWhitespace() { + // given + String input = " 123 "; + + // when & then + List result = InputConverter.convertStringToIntegerList(input); + assertThat(result) + .hasSize(3) + .containsExactly(1, 2, 3); + } + +} \ No newline at end of file