diff --git a/README.md b/README.md index 491aece1..3b41a87a 100644 --- a/README.md +++ b/README.md @@ -1 +1,100 @@ -# java-racingcar-precourse \ No newline at end of file +version 1.1 +# java-racingcar-precourse +### interface Observer +* update() : void +--- +### interface Subject +* addObserver(Observer) : void +* removeObserver(Observer) : void +* notifyObservers() : void +--- +### interface CarModelInterface +* getName() : String +* getProgress() : int +* setProgress() : void +--- +### interface RaceModelInterface +* getNumberOfCars() : int +* getNumberOfRounds() : int +* setNumberOfRounds() : void +* getCars() : List\ +--- +### interface CarServiceInterface +* move() : void +--- +### interface RaceServiceInterface +* getNumberOfCars() : int +* getNumberOfRounds() : int +* setNumberOfRounds() : void +--- +### interface RaceViewInterface +* userCarsInput() : void +* userRoundsInput() : void +* printProgress() : void +* printProcess() : void +* printResult() : void +--- +### interface RaceControllerInterface +* init() : void +* play() : void +* end() : void +* addCar(CarServiceInterface) : void +* setRound(int) : void +--- +### class CarTest +* tests +--- +### class RaceServiceTest +* tests +--- +### class Car - model +* name : String +* progress : int +* getName() : String +* getProgress() : int +* setProgress() : void +--- +### class Race : Subject, RaceServiceInterface - model +* numberOfRounds : int +* cars : List\ +* observers : List\ +* getNumberOfCars() : int +* getNumberOfRounds() : int +* setNumberOfRounds() : void +* getCars() : List\ +* addObserver(Observer) : void +* removeObserver(Observer) : void +* notifyObservers() : void +--- +### CarService : CarServiceInterface - service +* car : CarModelInterface +* goForward() : void +* move() : void +* nameValidation() : boolean +--- +### RaceService : RaceServiceInterface - service +* race : RaceModelInterface +* addCar(CarServiceInterface) : void +* setRound(int) : void +* roundValidation() : boolean +* addObserver(Observer) : void +* removeObserver(Observer) : void +* notifyObservers() : void +--- +### class RaceView : Observer, RaceViewInterface - view +* raceServiceInterface : RaceServiceInterface +* raceControllerInterface : RaceControllerInterface +* userCarsInput() : void +* userRoundsInput() : void +* printProgress() : void +* printProcess() : void +* printResult() : void +--- +### RaceController : RaceControllerInterface - controller +* race : RaceServiceInterface +* view : RaceViewInterface +* init() : void +* addCar(CarServiceInterface) : void +* setRound(int) : void +* play() : void +* end() : void diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep index e69de29b..945c9b46 100644 --- a/src/main/java/.gitkeep +++ b/src/main/java/.gitkeep @@ -0,0 +1 @@ +. \ 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..e43369f9 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,43 @@ +import controller.RaceController; +import controller.RaceControllerInterface; +import model.Race; +import observer.Observer; +import observer.Subject; +import service.RaceService; +import service.RaceServiceInterface; +import view.RaceView; +import view.RaceViewInterface; + +public class Application { + + public static void main(String[] args) { + // Race 객체 생성 + Race race = new Race(); + + // RaceService 객체 생성 + RaceService raceService = new RaceService(race, race); + // RaceServivce 인스턴스를 각 인터페이스에 맞게 변수 생성. 느슨한 결합을 위함 + RaceServiceInterface raceServiceInterface = raceService; + Subject subject = raceService; + + // RaceView 객체 생성 + RaceView raceView = new RaceView(raceServiceInterface); + // RaceView 인스턴스를 각 인터페이스에 맞게 변수 생성. 느슨한 결합을 위함. + RaceViewInterface raceViewInterface = raceView; + Observer observer = raceView; + + // subject(RaceService)에 observer(raceview) 등록. + // controller의 부담을 덜기 위함. + subject.addObserver(observer); + + // RaceController 생성 및 인터페이스 변수 생성 + RaceControllerInterface raceController = new RaceController(raceService, raceViewInterface); + // RaceView와 RaceController가 상호 참조하므로 seatter를 사용함. + raceViewInterface.setRaceControllerInterface(raceController); + + // 컨트롤러를 사용한 서비스 시작 + raceController.init(); + raceController.play(); + raceController.end(); + } +} diff --git a/src/main/java/controller/RaceController.java b/src/main/java/controller/RaceController.java new file mode 100644 index 00000000..5c4332a1 --- /dev/null +++ b/src/main/java/controller/RaceController.java @@ -0,0 +1,70 @@ +package controller; + +import service.RaceServiceInterface; +import view.RaceViewInterface; + +// 실제 사용자가 조작할 컨트롤러. 이 클래스만으로 모든 로직을 수행할 수 있어야 한다. +public class RaceController implements RaceControllerInterface { + + private RaceServiceInterface race; + private RaceViewInterface view; + + // 생성자 + public RaceController(RaceServiceInterface race, RaceViewInterface view) { + this.race = race; + this.view = view; + } + + // 시작 단계에서 호출하는 메서드 + @Override + public void init() { + view.userCarsInput(); + view.userRoundsInput(); + } + + // 중간 단계에서 호출하는 메서드 + @Override + public void play() { + view.printProcess(); + race.startRace(); + } + + // 마무리 단계에서 호출하는 메서드 + @Override + public void end() { + view.printResult(); + } + + // view에서 입력으로 들어온 차를 생성하도록 race에 전달 + @Override + public void addCars(String[] cars) { + try { + // 생성 시도 + race.addCars(cars); + } catch (IllegalArgumentException e) { + // 5글자를 넘은 경우, error 메시지를 출력하고 + view.printCarsErrorMessage(); + // 다시 view에 입력을 요청 + view.userCarsInput(); + } + } + + // view에서 입력으로 들어온 횟수를 저장하도록 race에 전달 + @Override + public void setRound(String numberOfRound) { + // 전달 시도 + try { + race.prepareRace(numberOfRound); + } catch (NumberFormatException e) { + // 숫자가 아닌 경우, error 메시지를 출력 + view.printRoundsFormatErrorMessage(); + // 다시 view에 입력을 요청 + view.userRoundsInput(); + } catch (IllegalArgumentException e) { + // 숫자가 너무 큰 경우, error 메시지를 출력 + view.printRoundsArgumentErrorMessage(); + // 다시 view에 입력을 요청 + view.userRoundsInput(); + } + } +} diff --git a/src/main/java/controller/RaceControllerInterface.java b/src/main/java/controller/RaceControllerInterface.java new file mode 100644 index 00000000..ee53f4be --- /dev/null +++ b/src/main/java/controller/RaceControllerInterface.java @@ -0,0 +1,20 @@ +package controller; + +// 실제 사용자가 조작할 컨트롤러 +public interface RaceControllerInterface { + + // 사용자가 조작하는 프로그램의 첫 부분 + public void init(); + + // 사용자가 조작하는 프로그램의 중간 부분 + public void play(); + + // 사용자가 조작하는 프로그램의 마지막 부분 + public void end(); + + // view로부터 입력을 받아서 service로 차들의 이름을 전송하는 메서드 + public void addCars(String[] cars); + + // view로부터 입력을 받아서 service로 round의 횟수를 전송하는 메서드 + public void setRound(String numberOfRound); +} diff --git a/src/main/java/model/Car.java b/src/main/java/model/Car.java new file mode 100644 index 00000000..e15ccbce --- /dev/null +++ b/src/main/java/model/Car.java @@ -0,0 +1,52 @@ +package model; + +// model과 service를 분리할 필요가 없다고 생각하여 model만 사용 +public class Car implements CarModelInterface { + + private final String name; + private int progress; + + public Car() { + name = ""; + progress = 0; + } + + public Car(String name, int progress) { + this.name = name; + this.progress = progress; + } + + private void setProgress(int progress) { + this.progress = progress; + } + + // getter + @Override + public String getName() { + return name; + } + + // getter + @Override + public int getProgress() { + return progress; + } + + // 한 칸 앞으로 가는 메서드 + private void goForward() { + setProgress(getProgress() + 1); + } + + // 난수에 따라 한 칸 전진할지 결정하는 메서드 + @Override + public void moveOrNot() { + // 랜덤 변수를 생성한 후 + int randomNumber = (int) (Math.random() * 10); + + // 만약 4 이상이라면 + if (randomNumber > 3) { + // 앞으로 전진한다. + goForward(); + } + } +} diff --git a/src/main/java/model/CarModelInterface.java b/src/main/java/model/CarModelInterface.java new file mode 100644 index 00000000..8f0455e8 --- /dev/null +++ b/src/main/java/model/CarModelInterface.java @@ -0,0 +1,14 @@ +package model; + +// 차 하나하나에 대한 model +public interface CarModelInterface { + + // car가 갖는 이름을 반환하는 메서드 + String getName(); + + // car의 진행도를 반환하는 메서드 + int getProgress(); + + // 차에 전진 혹은 정지를 명령하는 메서드 + void moveOrNot(); +} diff --git a/src/main/java/model/Race.java b/src/main/java/model/Race.java new file mode 100644 index 00000000..9a4c1038 --- /dev/null +++ b/src/main/java/model/Race.java @@ -0,0 +1,127 @@ +package model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import observer.Observer; +import observer.Subject; + +// model과 service를 분리해서 model에 관련된 로직만 갖도록 함. +public class Race implements Subject, RaceModelInterface { + + private int numberOfRounds; + private List cars; + private List observers; + + public Race() { + this.numberOfRounds = 0; + cars = new ArrayList<>(); + observers = new ArrayList<>(); + } + + // getter + @Override + public int getNumberOfCars() { + return cars.size(); + } + + // getter + @Override + public List getCars() { + return cars; + } + + // getter + @Override + public int getNumberOfRounds() { + return numberOfRounds; + } + + // setter + private void setNumberOfRounds(int numberOfRounds) { + this.numberOfRounds = numberOfRounds; + } + + // 제대로 된 입력이 들어왔는지 확인하는 메서드 + private void verifyNumberOfCars(String[] carsName) throws IllegalArgumentException { + // 빈 입력이 들어온 경우도 예외를 반환 + if (carsName.length == 0) { + throw new IllegalArgumentException(); + } + + // cars의 원소 중에 길이가 5 이상인 것이 있다면 예외를 반환 + boolean isWrongArgument = Arrays.stream(carsName).anyMatch(car -> car.length() > 5); + if (isWrongArgument) { + throw new IllegalArgumentException(); + } + } + + @Override + // 차를 추가하는 메서드 + public void addCars(String[] carsName) throws IllegalArgumentException { + // 유효성 검사부터 + verifyNumberOfCars(carsName); + // 검사가 끝났으면 객체 리스트를 생성 + List cars = Arrays.stream(carsName).map(carName -> new Car(carName, 0)) + .collect(Collectors.toList()); + // 추가 + this.cars.addAll(cars); + } + + // 경주 시작 전 round 설정하는 메서드 + @Override + public void prepareRace(String rounds) throws IllegalArgumentException { + // 수로 변환. + int numberOfRounds = Integer.parseInt(rounds); + // 유효성 검사 + verifyNumberOfRounds(numberOfRounds); + // setter를 통해 값 설정 + setNumberOfRounds(numberOfRounds); + } + + // round 횟수에 대한 유효성 검사 + private void verifyNumberOfRounds(int numberOfRounds) throws IllegalArgumentException { + // 100보다 큰 입력이 들어온 경우 + if (numberOfRounds > 100) { + throw new IllegalArgumentException(); + } + } + + // 차들의 정보가 바뀐 후에 실행되는 메서드. + private void carsChanged() { + // 옵저버들에게 알림 + // 지금은 하나의 역할만 수행하지만, 단일 책임 + 확장을 위해 메서드를 분리 + notifyObservers(); + } + + // cars를 하나씩 출발시키는 작업을 n번 반복하는 메서드 + @Override + public void startRace() { + // 횟수만큼 반복 + for (int i = 0; i < numberOfRounds; i++) { + // 모든 차들에 대해 경주 시작 + cars.stream().forEach(car -> car.moveOrNot()); + // 차들의 변경이 일어났으므로 이를 처리하는 메서드를 호출 + carsChanged(); + } + } + + // observer를 추가하는 메서드 + @Override + public void addObserver(Observer observer) { + observers.add(observer); + } + + // observer를 삭제하는 메서드 + @Override + public void removeObserver(Observer observer) { + observers.remove(observer); + } + + // observer들을 update하는 메서드 + @Override + public void notifyObservers() { + observers.stream().forEach(observer -> observer.update()); + } +} diff --git a/src/main/java/model/RaceModelInterface.java b/src/main/java/model/RaceModelInterface.java new file mode 100644 index 00000000..07a30cbd --- /dev/null +++ b/src/main/java/model/RaceModelInterface.java @@ -0,0 +1,30 @@ +package model; + +import java.util.List; + +// 여러 대의 차가 배치된 경기에 대한 model +public interface RaceModelInterface { + + // 차의 총 개수를 반환하는 메서드 + int getNumberOfCars(); + + // 라운드 횟수를 반환하는 메서드 + int getNumberOfRounds(); + + // race 시작 전, controller를 통해 받은 round의 횟수를 저장하는 메서드 + // 조건 검사가 들어 있고, setter를 지양하기 위해 + // 해당 메서드에서는 조건 검사를 마치고 private으로 선언된 setter를 이용해 round를 설정한다. + void prepareRace(String rounds); + + // car 목록을 반환하는 메서드 + List getCars(); + + // car 목록을 추가하는 메서드 + // 마찬가지로 조건 검사 및 setter 지양 목적 + void addCars(String[] carsName); + + // race를 시작하는 메서드 + // service와 model 중 어디에 배치할지 고민했지만, + // model의 변수(cars)에 값의 변경을 요구하기 때문에 책임을 분리하고자 model에 배치했다. + void startRace(); +} diff --git a/src/main/java/observer/Observer.java b/src/main/java/observer/Observer.java new file mode 100644 index 00000000..682eb72f --- /dev/null +++ b/src/main/java/observer/Observer.java @@ -0,0 +1,8 @@ +package observer; + +// Observer pattern을 위한 observer +public interface Observer { + + // 내부의 변수를 참조해서 update하면, 바로 view에 변화를 출력한다. + void update(); +} diff --git a/src/main/java/observer/Subject.java b/src/main/java/observer/Subject.java new file mode 100644 index 00000000..abb54041 --- /dev/null +++ b/src/main/java/observer/Subject.java @@ -0,0 +1,14 @@ +package observer; + +// Observer pattern의 subject +public interface Subject { + + // 관찰 대상인 클래스에서, 관찰하는 클래스를 addObserver하면 자동으로 변화를 감지 + void addObserver(Observer observer); + + // 사용하지는 않았지만, Observer를 제거하는 메서드도 존재 + void removeObserver(Observer observer); + + // Observer의 update를 직접 호출해서 값의 변화가 일어났음을 알리는 메서드 + void notifyObservers(); +} diff --git a/src/main/java/service/RaceService.java b/src/main/java/service/RaceService.java new file mode 100644 index 00000000..c1286fa6 --- /dev/null +++ b/src/main/java/service/RaceService.java @@ -0,0 +1,106 @@ +package service; + +import java.util.List; +import java.util.stream.Collectors; +import model.CarModelInterface; +import model.RaceModelInterface; +import observer.Observer; +import observer.Subject; + +// model과 service를 분리해서 service는 비즈니스 로직만 갖도록 함. +public class RaceService implements Subject, RaceServiceInterface { + + private RaceModelInterface race; + private Subject subject; + + // 생성자 + public RaceService(RaceModelInterface race, Subject subject) { + this.race = race; + this.subject = subject; + } + + // getter + @Override + public int getNumberOfCars() { + return race.getNumberOfCars(); + } + + // getter + @Override + public int getNumberOfRounds() { + return race.getNumberOfRounds(); + } + + // getter + @Override + public List getCars() { + return race.getCars(); + } + + // controller가 넘겨준 차 목록을 추가하는 메서드 + @Override + public void addCars(String[] carsName) throws IllegalArgumentException { + race.addCars(carsName); + } + + // 우승자의 진행도를 반환하는 메서드 + private int getWinnersProgress() { + // 차들의 정보를 가져와서 + List carList = race.getCars(); + + // 진행도의 최댓값을 구한다. + int winnersProgress = 0; + for (CarModelInterface car : carList) { + winnersProgress = Math.max(winnersProgress, car.getProgress()); + } + + return winnersProgress; + } + + // 비즈니스 로직: 우승자의 목록이 필요하다. + @Override + public List getWinners() { + // 차들의 정보를 가져오고 + List carList = race.getCars(); + // 우승자의 진행도를 가져온다. + int winnersProgress = getWinnersProgress(); + + // 우승자의 진행도를 바탕으로 우승자 목록을 stream으로 가져온다. + List winners = carList.stream() + .filter(car -> car.getProgress() == winnersProgress).collect(Collectors.toList()); + + return winners; + } + + // controller에게 받은 round 정보를 설정하는 메서드 + @Override + public void prepareRace(String rounds) + throws IllegalArgumentException { + // rounds를 전달. + race.prepareRace(rounds); + } + + // cars를 하나씩 출발시키는 작업을 n번 반복하는 메서드 + @Override + public void startRace() { + race.startRace(); + } + + // observer를 추가하는 메서드 + @Override + public void addObserver(Observer observer) { + subject.addObserver(observer); + } + + // observer를 삭제하는 메서드 + @Override + public void removeObserver(Observer observer) { + subject.removeObserver(observer); + } + + // observer들을 update하는 메서드 + @Override + public void notifyObservers() { + subject.notifyObservers(); + } +} diff --git a/src/main/java/service/RaceServiceInterface.java b/src/main/java/service/RaceServiceInterface.java new file mode 100644 index 00000000..51a0afbf --- /dev/null +++ b/src/main/java/service/RaceServiceInterface.java @@ -0,0 +1,31 @@ +package service; + +import java.util.List; +import model.CarModelInterface; + +// 여러 대의 차가 배치된 경기에 대한 비즈니스 로직을 담은 service +public interface RaceServiceInterface { + + // model로부터 car의 총 개수를 받아서 다시 반환하는 메서드 + int getNumberOfCars(); + + // model로부터 round의 총 횟수를 받아서 다시 반환하는 메서드 + int getNumberOfRounds(); + + // model로부터 car의 정보를 받아서 다시 반환하는 메서드 + List getCars(); + + // 비즈니스 로직: model로부터 여러 정보를 받아 winner를 뽑아내는 메서드. + // 이 부분은 model의 내부 구조에 대해 자세히 알 필요도 없고 + // setter를 사용해서 값을 변경하지도 않으므로, service에 배치했다. + List getWinners(); + + // model에게 차의 이름 목록을 넘겨주어 차를 추가하는 메서드 + void addCars(String[] carsName); + + // model에게 round의 횟수를 넘겨주어 경기를 준비하는 메서드 + void prepareRace(String rounds); + + // model에게 요청하여 실제로 경기를 시작하게 하는 메서드 + void startRace(); +} diff --git a/src/main/java/view/RaceView.java b/src/main/java/view/RaceView.java new file mode 100644 index 00000000..24706f96 --- /dev/null +++ b/src/main/java/view/RaceView.java @@ -0,0 +1,120 @@ +package view; + +import controller.RaceControllerInterface; +import java.util.List; +import java.util.Scanner; +import java.util.stream.IntStream; +import model.CarModelInterface; +import observer.Observer; +import service.RaceServiceInterface; + +public class RaceView implements Observer, RaceViewInterface { + + // View가 참조할 Service와 Controller. 조작은 절대 금지. + private RaceServiceInterface raceServiceInterface; + private RaceControllerInterface raceControllerInterface = null; + + // 생성자 + public RaceView(RaceServiceInterface raceServiceInterface) { + this.raceServiceInterface = raceServiceInterface; + } + + // setter + public void setRaceControllerInterface(RaceControllerInterface raceControllerInterface) { + this.raceControllerInterface = raceControllerInterface; + } + + // 옵저버 패턴에서 옵저버가 갖는 메서드 + @Override + public void update() { + // update가 발생했으면 즉시 출력 + printProgress(); + } + + // 자동차 이름을 입력받는 메서드 + @Override + public void userCarsInput() { + // 안내문 출력 + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + + // 유저의 입력을 받고 token으로 쪼개기 + Scanner sc = new Scanner(System.in); + String userInput = sc.nextLine(); + String[] cars = userInput.split(","); + + // Controller에게 유저의 입력을 전달 + raceControllerInterface.addCars(cars); + } + + // 사용자의 횟수를 입력받는 메서드 + @Override + public void userRoundsInput() { + // 안내문 출력 + System.out.println("시도할 횟수는 몇 회인가요? (100회 이하만 가능합니다.)"); + + // 유저의 입력을 받기 + Scanner sc = new Scanner(System.in); + String userInput = sc.nextLine(); + + // Controller에게 유저의 입력을 전달 + raceControllerInterface.setRound(userInput); + } + + // progress를 받으면 막대기로 돌려주는 메서드 + private String getProgressBar(int progress) { + // 빠른 처리를 위한 Stringbuilder + StringBuilder stringBuilder = new StringBuilder(); + IntStream.range(0, progress).forEach(i -> stringBuilder.append('-')); + + return stringBuilder.toString(); + } + + // 한 번 진행의 결과를 보여주는 메서드 + @Override + public void printProgress() { + // raceService를 통해 차 목록을 받아와서 + RaceServiceInterface raceService = raceServiceInterface; + List carList = raceService.getCars(); + // stream을 이용하여 양식에 맞게 출력 + carList.stream() + .forEach(car -> System.out.println(car.getName() + " : " + getProgressBar(car.getProgress()))); + System.out.println(); + } + + // 과정을 담당하는 메서드 + @Override + public void printProcess() { + System.out.println("\n실행 결과"); + } + + // 결과를 출력하는 메서드 + @Override + public void printResult() { + // service 인터페이스를 통해 값 참조 + List winners = raceServiceInterface.getWinners(); + // 문자열을 빠르게 붙이기 위해 StringBuilder를 사용 + StringBuilder stringBuilder = new StringBuilder(); + + // 우승자들의 정보를 String으로 변환한다. + IntStream.range(0, winners.size() - 1).forEach(i -> stringBuilder.append(winners.get(i).getName()).append(", ")); + stringBuilder.append(winners.get(winners.size() - 1).getName()); + + System.out.println("최종 우승자 : " + stringBuilder); + } + + // 에러 메시지 출력 메서드들 + @Override + public void printCarsErrorMessage() { + System.out.println("[ERROR] 입력한 차의 이름이 올바르지 않습니다. 차의 이름은 5자 이하만 가능합니다."); + } + + @Override + public void printRoundsArgumentErrorMessage() { + System.out.println("[ERROR] 횟수는 100 이하여야 합니다."); + } + + @Override + public void printRoundsFormatErrorMessage() { + System.out.println("[ERROR] 횟수는 수로 입력해야 합니다."); + } +} diff --git a/src/main/java/view/RaceViewInterface.java b/src/main/java/view/RaceViewInterface.java new file mode 100644 index 00000000..d2df22b7 --- /dev/null +++ b/src/main/java/view/RaceViewInterface.java @@ -0,0 +1,35 @@ +package view; + +import controller.RaceControllerInterface; + +// 경기에 대해 사용자의 입력, 사용자에게 보여줄 출력을 담당하는 view +public interface RaceViewInterface { + + // user로부터 차의 이름들을 받는 메서드 + void userCarsInput(); + + // user로부터 round 횟수를 받는 메서드 + void userRoundsInput(); + + // view <-> controller 상호 참조를 위해 setter로 뒤늦게 추가 + // 좋은 방법은 아닌 것 같다 + void setRaceControllerInterface(RaceControllerInterface raceControllerInterface); + + // 한 횟수에서 차들의 진행도를 출력. observer의 update가 호출하는 메서드 + void printProgress(); + + // 중간 안내문을 출력하는 메서드 + void printProcess(); + + // 결과를 출력하는 메서드 + void printResult(); + + // 사용자가 입력한 차의 정보가 잘못된 경우 경고를 출력하는 메서드 (5자 초과, 빈 입력) + void printCarsErrorMessage(); + + // 사용자가 입력한 round가 잘못된 경우 경고를 출력하는 메서드 (100 초과) + void printRoundsArgumentErrorMessage(); + + // 사용자가 입력한 round가 잘못된 경우 경고를 출력하는 메서드 (parse 불가) + void printRoundsFormatErrorMessage(); +} diff --git a/src/test/java/RaceServiceTest.java b/src/test/java/RaceServiceTest.java new file mode 100644 index 00000000..af977aa1 --- /dev/null +++ b/src/test/java/RaceServiceTest.java @@ -0,0 +1,155 @@ +import java.util.List; +import java.util.stream.IntStream; +import model.CarModelInterface; +import model.Race; +import org.junit.jupiter.api.Test; +import org.assertj.core.api.Assertions; +import service.RaceService; +import service.RaceServiceInterface; + +/* +구체적 로직을 갖는 RaceService에 대한 테스트. +단순한 getter, setter에 대한 테스트는 생략하였습니다. +service와 model을 분리하였으므로 model에 대한 테스트 없이 service에 대한 테스트만 진행합니다. +또한 RaceService의 로직에서 car model의 모든 기능을 사용하므로 car model에 대한 테스트도 생략합니다. +*/ + +public class RaceServiceTest { + + // addCars를 통해 삽입이 잘 됐는지 확인 + @Test + void addCarsTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String[] carsName = {"jih", "hyu", "abc", "xyz"}; + + // 삽입 과정 + raceService.addCars(carsName); + // 개수 구하고 + int numberOfCars = raceService.getNumberOfCars(); + // 삽입한 배열과 리스트의 원소가 동일하면 true를 반환하도록 함. + boolean isEqual = IntStream.range(0, numberOfCars) + .allMatch(i -> carsName[i].equals(raceService.getCars().get(i).getName())); + + // 삽입 배열과 리스트의 개수가 맞는지 확인 + Assertions.assertThat(numberOfCars).isEqualTo(carsName.length); + // 삽입한 배열이 리스트에 잘 들어갔는지 확인 + Assertions.assertThat(isEqual).isTrue(); + } + + // addCars를 통해 5자 초과의 차 이름을 넣었을 때 예외 처리가 되는지 확인 + @Test + void addCarsNameErrorTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String[] carsErrorName = {"abcdef", "abcd"}; + + // 잘못된 경우에 대한 테스트 (5자 이상) + Assertions.assertThatThrownBy(() -> raceService.addCars(carsErrorName)) + .isInstanceOf(IllegalArgumentException.class); + } + + // addCars를 통해 아무 입력도 넣지 않았을 때 예외 처리가 되는지 확인 + @Test + void addCarsEmptyErrorTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String[] empty = {}; + + // 잘못된 경우에 대한 테스트 (빈 입력) + Assertions.assertThatThrownBy(() -> raceService.addCars(empty)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void prepareRaceTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String round = "5"; + + // race 준비(round 정보 설정) + raceService.prepareRace(round); + int numberOfRounds = raceService.getNumberOfRounds(); + + // round가 잘 들어갔는지 테스트 + Assertions.assertThat(numberOfRounds).isEqualTo(Integer.parseInt(round)); + } + + @Test + void prepareRaceParseErrorTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String round = "a"; + + // 잘못된 경우에 대해 테스트 (parse 문제) + Assertions.assertThatCode(() -> raceService.prepareRace(round)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void prepareRaceRangeErrorTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String round = "101"; + + // 잘못된 경우에 대해 테스트 (범위 문제) + Assertions.assertThatCode(() -> raceService.prepareRace(round)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void startRaceTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String[] carsName = {"jih", "hyu", "abc", "xyz", "aaa", "bbb", "ccc", "ddd", "eee", "fff", + "ggg", "hhh"}; + + // race 준비 및 시작 + raceService.addCars(carsName); + // 충분히 많은 시행에서 모두의 진행도가 동일하다면 어딘가 로직이 잘못된 것 + raceService.prepareRace("100"); + raceService.startRace(); + boolean isAllEqual = raceService.getCars().stream() + .allMatch(car -> raceService.getCars().get(0).getProgress() == car.getProgress()); + + // 모두의 진행도가 동일한지 확인 + Assertions.assertThat(isAllEqual).isFalse(); + } + + @Test + void getWinnersTest() { + // 객체와 삽입할 배열을 준비 + Race race = new Race(); + RaceServiceInterface raceService = new RaceService(race, race); + String[] carsName = {"jih", "hyu", "abc", "xyz"}; + + // 삽입 + raceService.addCars(carsName); + // round 설정 (= 3) + raceService.prepareRace("3"); + // 경주 시작 + raceService.startRace(); + // 승자 목록 받아옴. getWinners 테스트 + List winners = raceService.getWinners(); + // 승자 중 한명의 진행도를 받아옴. + int winnerProgress = winners.get(0).getProgress(); + // 우선 승자들끼리 진행도가 모두 같은지 확인하는 변수 생성 + boolean isWinnersProgressAllEqual = winners.stream() + .allMatch(car -> car.getProgress() == winnerProgress); + // getWinners로 받아온 승자의 진행도가 실제로 가장 높은지 확인하는 변수 생성 + boolean isWinnerProgressTheHighest = raceService.getCars().stream() + .allMatch(car -> car.getProgress() <= winnerProgress); + + // 승자들끼리의 진행도를 확인 + Assertions.assertThat(isWinnersProgressAllEqual).isTrue(); + // getWinners로 받아온 승자가 진짜 승자인지 확인 + Assertions.assertThat(isWinnerProgressTheHighest).isTrue(); + } +}