diff --git a/build.gradle b/build.gradle index 721b6af13..dac60ff00 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' runtimeOnly 'com.h2database:h2' + + implementation 'org.springframework.boot:spring-boot-starter-validation' } tasks.named('test') { diff --git a/src/main/java/racingcar/controller/RacingCarController.java b/src/main/java/racingcar/controller/RacingCarController.java index ce2d05fdb..d69299074 100644 --- a/src/main/java/racingcar/controller/RacingCarController.java +++ b/src/main/java/racingcar/controller/RacingCarController.java @@ -1,21 +1,12 @@ package racingcar.controller; -import racingcar.domain.Car; import racingcar.domain.RacingCars; import racingcar.util.NumberGenerator; import racingcar.view.InputView; import racingcar.view.OutputView; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - public class RacingCarController { - private static final int START_POSITION = 0; - private final OutputView outputView; private final InputView inputView; private final NumberGenerator numberGenerator; @@ -27,63 +18,24 @@ public RacingCarController(final NumberGenerator numberGenerator) { } public void run() { - String[] carNames = readCarNamesStep(); - RacingCars racingCars = generateRacingCarsStep(carNames); - int tryNum = readTryNumStep(); - race(tryNum, racingCars); - showWinners(racingCars); - } + final String carNames = inputView.readCarNames(); + int trialCount = inputView.readTryNum(); - private String[] readCarNamesStep() { - outputView.printReadCarNamesMessage(); - String[] carNames = inputView.readCarNames(); - return carNames; + RacingCars racingCars = moveCars(carNames, trialCount); + showRaceResult(racingCars); } - private RacingCars generateRacingCarsStep(String[] carNames) { - List cars = generateCars(carNames); - return null; + private void showRaceResult(final RacingCars racingCars) { + showWinners(racingCars); } - private List generateCars(String[] carNames) { - return Arrays.stream(carNames) - .map(carName -> new Car(carName, START_POSITION)) - .collect(Collectors.toUnmodifiableList()); + private RacingCars moveCars(String carNames, int trialCount) { + RacingCars racingCars = RacingCars.makeCars(carNames); + racingCars.moveAllCars(trialCount, numberGenerator); + return racingCars; } private void showWinners(RacingCars racingCars) { - List winners = convertWinnersNameForPrint(racingCars.getWinners()); - outputView.printWinners(winners); - } - - private void race(int tryNum, RacingCars racingCars) { - outputView.printRacingResultMessage(); - for (int repeatIndex = 0; repeatIndex < tryNum; repeatIndex++) { - List currentCars = racingCars.getCars(); - for (Car currentCar : currentCars) { - int randomValue = numberGenerator.generate(); - currentCar.move(randomValue); - } - outputView.printCurrentRacingCarsPosition(convertRacingCarsResultForPrint(currentCars)); - } - } - - private int readTryNumStep() { - outputView.printReadTryNumMessage(); - return inputView.readTryNum(); - } - - private Map convertRacingCarsResultForPrint(List currentCars) { - Map racingCarsResult = new HashMap<>(); - for (Car currentCar : currentCars) { - racingCarsResult.put(currentCar.getName(), currentCar.getPosition()); - } - return racingCarsResult; - } - - private List convertWinnersNameForPrint(List winners) { - return winners.stream() - .map(Car::getName) - .collect(Collectors.toList()); + outputView.printWinners(racingCars); } } diff --git a/src/main/java/racingcar/controller/RacingCarWebController.java b/src/main/java/racingcar/controller/RacingCarWebController.java index 80d30e20b..019c86ebe 100644 --- a/src/main/java/racingcar/controller/RacingCarWebController.java +++ b/src/main/java/racingcar/controller/RacingCarWebController.java @@ -1,30 +1,35 @@ package racingcar.controller; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import racingcar.controller.dto.GameInfoRequest; -import racingcar.controller.dto.RaceResultResponse; -import racingcar.controller.validator.GameOptionValidator; import racingcar.service.RaceResultService; +import racingcar.service.dto.GameInfoRequest; +import racingcar.service.dto.RaceResultResponse; + +import java.util.List; @RestController public class RacingCarWebController { - private final GameOptionValidator gameOptionValidator; private final RaceResultService raceResultService; - public RacingCarWebController(final GameOptionValidator gameOptionValidator, - final RaceResultService raceResultService) { - this.gameOptionValidator = gameOptionValidator; + public RacingCarWebController(final RaceResultService raceResultService) { this.raceResultService = raceResultService; } @PostMapping("/plays") - public RaceResultResponse registerRaceResult(@RequestBody final GameInfoRequest gameInfoRequest) { - - gameOptionValidator.validateGameOption(gameInfoRequest); - + @ResponseStatus(code = HttpStatus.CREATED) + public RaceResultResponse registerRaceResult(@Validated @RequestBody final GameInfoRequest gameInfoRequest) { return raceResultService.createRaceResult(gameInfoRequest); } + + @GetMapping("/plays") + public List showRaceResult() { + return raceResultService.searchRaceResult(); + } } diff --git a/src/main/java/racingcar/controller/dto/GameInfoRequest.java b/src/main/java/racingcar/controller/dto/GameInfoRequest.java deleted file mode 100644 index 3857c43c9..000000000 --- a/src/main/java/racingcar/controller/dto/GameInfoRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package racingcar.controller.dto; - -public class GameInfoRequest { - - private String names; - private int count; - - private GameInfoRequest() { - } - - public String getNames() { - return names; - } - - public int getCount() { - return count; - } -} diff --git a/src/main/java/racingcar/controller/validator/GameOptionValidator.java b/src/main/java/racingcar/controller/validator/GameOptionValidator.java deleted file mode 100644 index d33e04333..000000000 --- a/src/main/java/racingcar/controller/validator/GameOptionValidator.java +++ /dev/null @@ -1,28 +0,0 @@ -package racingcar.controller.validator; - -import org.springframework.stereotype.Component; -import racingcar.controller.dto.GameInfoRequest; - -@Component -public class GameOptionValidator { - - private static final String CAR_NAMES_BLANK_ERROR = "[ERROR] 경주할 자동차 이름이 입력되지 않았습니다."; - private static final String TRY_NUM_NOT_POSITIVE_ERROR = "[ERROR] 시도할 횟수는 1 이상이어야 합니다."; - - public void validateGameOption(final GameInfoRequest gameInfoRequest) { - validateNames(gameInfoRequest.getNames()); - validateTrialCount(gameInfoRequest.getCount()); - } - - private void validateNames(String names) { - if (names.length() < 1) { - throw new IllegalArgumentException(CAR_NAMES_BLANK_ERROR); - } - } - - private void validateTrialCount(int trialCount) { - if (trialCount <= 0) { - throw new IllegalArgumentException(TRY_NUM_NOT_POSITIVE_ERROR); - } - } -} diff --git a/src/main/java/racingcar/dao/CarDao.java b/src/main/java/racingcar/dao/CarDao.java new file mode 100644 index 000000000..2ed0fe33d --- /dev/null +++ b/src/main/java/racingcar/dao/CarDao.java @@ -0,0 +1,43 @@ +package racingcar.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import racingcar.entity.CarEntity; + +import java.util.List; + +@Repository +public class CarDao { + + private final JdbcTemplate jdbcTemplate; + private final SimpleJdbcInsert simpleJdbcInsert; + private final RowMapper rowMapper = (rs, rowNum) -> + new CarEntity( + rs.getLong("id"), + rs.getString("name"), + rs.getInt("position"), + rs.getLong("race_result_id"), + rs.getBoolean("winner"), + rs.getTimestamp("created_at").toLocalDateTime() + ); + + public CarDao(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.simpleJdbcInsert = + new SimpleJdbcInsert(jdbcTemplate) + .withTableName("CAR") + .usingGeneratedKeyColumns("id"); + } + + public void save(final List carEntities) { + simpleJdbcInsert.executeBatch(SqlParameterSourceUtils.createBatch(carEntities)); + } + + public List findAll() { + final String sql = "select * from CAR"; + return jdbcTemplate.query(sql, rowMapper); + } +} diff --git a/src/main/java/racingcar/dao/RaceResultDao.java b/src/main/java/racingcar/dao/RaceResultDao.java new file mode 100644 index 000000000..014ec1dff --- /dev/null +++ b/src/main/java/racingcar/dao/RaceResultDao.java @@ -0,0 +1,25 @@ +package racingcar.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import racingcar.entity.RaceResultEntity; + +@Repository +public class RaceResultDao { + + private final SimpleJdbcInsert simpleJdbcInsert; + + public RaceResultDao(final JdbcTemplate jdbcTemplate) { + this.simpleJdbcInsert = + new SimpleJdbcInsert(jdbcTemplate) + .withTableName("RACE_RESULT") + .usingGeneratedKeyColumns("id"); + } + + public Long save(final RaceResultEntity raceResultEntity) { + return simpleJdbcInsert.executeAndReturnKey(new BeanPropertySqlParameterSource(raceResultEntity)) + .longValue(); + } +} diff --git a/src/main/java/racingcar/dao/car/CarDao.java b/src/main/java/racingcar/dao/car/CarDao.java deleted file mode 100644 index 9b12990fe..000000000 --- a/src/main/java/racingcar/dao/car/CarDao.java +++ /dev/null @@ -1,31 +0,0 @@ -package racingcar.dao.car; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; -import racingcar.dao.car.dto.CarRegisterRequest; - -@Repository -public class CarDao { - - private final JdbcTemplate jdbcTemplate; - - public CarDao(final JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public void save(final CarRegisterRequest carRegisterRequest) { - final SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate); - - jdbcInsert.withTableName("CAR").usingGeneratedKeyColumns("id"); - - final SqlParameterSource params = new MapSqlParameterSource() - .addValue("name", carRegisterRequest.getName()) - .addValue("position", carRegisterRequest.getPosition()) - .addValue("play_result_id", carRegisterRequest.getPlayResultId()); - - jdbcInsert.execute(params); - } -} diff --git a/src/main/java/racingcar/dao/car/dto/CarRegisterRequest.java b/src/main/java/racingcar/dao/car/dto/CarRegisterRequest.java deleted file mode 100644 index ffa9a0db9..000000000 --- a/src/main/java/racingcar/dao/car/dto/CarRegisterRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package racingcar.dao.car.dto; - -public class CarRegisterRequest { - - private final String name; - private final int position; - private final int playResultId; - - public CarRegisterRequest(final String name, final int position, final int playResultId) { - this.name = name; - this.position = position; - this.playResultId = playResultId; - } - - public String getName() { - return name; - } - - public int getPosition() { - return position; - } - - public int getPlayResultId() { - return playResultId; - } -} diff --git a/src/main/java/racingcar/dao/raceresult/RaceResultDao.java b/src/main/java/racingcar/dao/raceresult/RaceResultDao.java deleted file mode 100644 index cd8256089..000000000 --- a/src/main/java/racingcar/dao/raceresult/RaceResultDao.java +++ /dev/null @@ -1,34 +0,0 @@ -package racingcar.dao.raceresult; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; -import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; -import org.springframework.stereotype.Repository; -import racingcar.dao.raceresult.dto.RaceResultRegisterRequest; - -import java.time.LocalDateTime; - -@Repository -public class RaceResultDao { - - private final JdbcTemplate jdbcTemplate; - - public RaceResultDao(final JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public int save(final RaceResultRegisterRequest raceResultRegisterRequest) { - - final SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate); - - jdbcInsert.withTableName("RACE_RESULT").usingGeneratedKeyColumns("id"); - - final SqlParameterSource params = new MapSqlParameterSource() - .addValue("trial_count", raceResultRegisterRequest.getTrialCount()) - .addValue("winners", raceResultRegisterRequest.getWinners()) - .addValue("created_at", LocalDateTime.now()); - - return jdbcInsert.executeAndReturnKey(params).intValue(); - } -} diff --git a/src/main/java/racingcar/dao/raceresult/dto/RaceResultRegisterRequest.java b/src/main/java/racingcar/dao/raceresult/dto/RaceResultRegisterRequest.java deleted file mode 100644 index 11be9d433..000000000 --- a/src/main/java/racingcar/dao/raceresult/dto/RaceResultRegisterRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package racingcar.dao.raceresult.dto; - -public class RaceResultRegisterRequest { - - private final int trialCount; - private final String winners; - - public RaceResultRegisterRequest(final int trialCount, final String winners) { - this.trialCount = trialCount; - this.winners = winners; - } - - public int getTrialCount() { - return trialCount; - } - - public String getWinners() { - return winners; - } -} diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java index 719e5abd2..4496c4779 100644 --- a/src/main/java/racingcar/domain/Car.java +++ b/src/main/java/racingcar/domain/Car.java @@ -47,6 +47,10 @@ public boolean isSamePositionCar(Car maxPositionCar) { return this.position == maxPositionCar.position; } + public boolean isSameCar(Car car) { + return this.name.equals(car.name); + } + public String getName() { return name; } diff --git a/src/main/java/racingcar/domain/RacingCars.java b/src/main/java/racingcar/domain/RacingCars.java index 94a1e2313..6f0706313 100644 --- a/src/main/java/racingcar/domain/RacingCars.java +++ b/src/main/java/racingcar/domain/RacingCars.java @@ -76,6 +76,11 @@ private List getMaxPositionCars(Car maxPositionCar) { .collect(Collectors.toUnmodifiableList()); } + public boolean isWinner(Car car) { + return getWinners().stream() + .anyMatch(it -> it.isSameCar(car)); + } + public List getCars() { return Collections.unmodifiableList(cars); } diff --git a/src/main/java/racingcar/domain/RacingGame.java b/src/main/java/racingcar/domain/RacingGame.java new file mode 100644 index 000000000..ae728156b --- /dev/null +++ b/src/main/java/racingcar/domain/RacingGame.java @@ -0,0 +1,46 @@ +package racingcar.domain; + +import racingcar.util.NumberGenerator; + +import java.util.List; + +public class RacingGame { + + private final RacingCars racingCars; + private final NumberGenerator numberGenerator; + private final Integer trialCount; + + public RacingGame(final RacingCars racingCars, final NumberGenerator numberGenerator, final Integer trialCount) { + this.racingCars = racingCars; + this.numberGenerator = numberGenerator; + this.trialCount = trialCount; + } + + public static RacingGame readyToRacingGame( + final String names, + final NumberGenerator numberGenerator, + final Integer trialCount + ) { + return new RacingGame(RacingCars.makeCars(names), numberGenerator, trialCount); + } + + public void play() { + racingCars.moveAllCars(trialCount, numberGenerator); + } + + public List determineWinners() { + return racingCars.getWinners(); + } + + public List getParticipantAllCar() { + return racingCars.getCars(); + } + + public boolean isWinner(Car car) { + return racingCars.isWinner(car); + } + + public Integer getTrialCount() { + return trialCount; + } +} diff --git a/src/main/java/racingcar/entity/CarEntity.java b/src/main/java/racingcar/entity/CarEntity.java new file mode 100644 index 000000000..7d5e2747f --- /dev/null +++ b/src/main/java/racingcar/entity/CarEntity.java @@ -0,0 +1,54 @@ +package racingcar.entity; + +import java.time.LocalDateTime; + +public class CarEntity { + + private final Long id; + private final String name; + private final int position; + private final Long raceResultId; + private final boolean winner; + private final LocalDateTime createdAt; + + public CarEntity(final Long id, final String name, + final int position, final Long raceResultId, + final boolean winner, final LocalDateTime createdAt) { + this.id = id; + this.name = name; + this.position = position; + this.raceResultId = raceResultId; + this.winner = winner; + this.createdAt = createdAt; + } + + public CarEntity(final String name, final int position, + final Long raceResultId, final boolean winner, + final LocalDateTime createdAt) { + this(null, name, position, raceResultId, winner, createdAt); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } + + public Long getRaceResultId() { + return raceResultId; + } + + public boolean isWinner() { + return winner; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/racingcar/entity/RaceResultEntity.java b/src/main/java/racingcar/entity/RaceResultEntity.java new file mode 100644 index 000000000..98998e006 --- /dev/null +++ b/src/main/java/racingcar/entity/RaceResultEntity.java @@ -0,0 +1,39 @@ +package racingcar.entity; + +import java.time.LocalDateTime; + +public class RaceResultEntity { + + private final Long id; + private final int trialCount; + private final LocalDateTime createdAt; + + public RaceResultEntity( + final Long id, + final int trialCount, + final LocalDateTime createdAt + ) { + this.id = id; + this.trialCount = trialCount; + this.createdAt = createdAt; + } + + public RaceResultEntity( + final int trialCount, + final LocalDateTime createdAt + ) { + this(null, trialCount, createdAt); + } + + public Long getId() { + return id; + } + + public int getTrialCount() { + return trialCount; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/racingcar/global/exception/ExceptionHandlerController.java b/src/main/java/racingcar/global/exception/ExceptionHandlerController.java new file mode 100644 index 000000000..470ac2953 --- /dev/null +++ b/src/main/java/racingcar/global/exception/ExceptionHandlerController.java @@ -0,0 +1,37 @@ +package racingcar.global.exception; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import racingcar.global.exception.response.DefaultExceptionResponse; +import racingcar.global.exception.response.ExceptionResponse; + +@RestControllerAdvice +public class ExceptionHandlerController { + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ExceptionResponse handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception + ) { + return ExceptionResponse.of(ExceptionStatus.INVALID_INPUT_VALUE_EXCEPTION, exception); + } + + @ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class}) + public ResponseEntity handleDefaultException( + Exception exception + ) { + return ResponseEntity.badRequest() + .body(DefaultExceptionResponse.from(exception)); + } + + @ExceptionHandler(DataAccessResourceFailureException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ExceptionResponse handleDataAccessException() { + return ExceptionResponse.of(ExceptionStatus.TEMPORARY_DATABASE_CONNECTION_EXCEPTION); + } +} diff --git a/src/main/java/racingcar/global/exception/ExceptionStatus.java b/src/main/java/racingcar/global/exception/ExceptionStatus.java new file mode 100644 index 000000000..0a95669ca --- /dev/null +++ b/src/main/java/racingcar/global/exception/ExceptionStatus.java @@ -0,0 +1,34 @@ +package racingcar.global.exception; + +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +public enum ExceptionStatus { + + INVALID_INPUT_VALUE_EXCEPTION(400, "유효하지 않는 입력 값입니다.", BAD_REQUEST), + TEMPORARY_DATABASE_CONNECTION_EXCEPTION(512, "데이터베이스에 문제가 생겼습니다.", HttpStatus.INTERNAL_SERVER_ERROR); + + + private final int status; + private final String message; + private final HttpStatus httpStatus; + + ExceptionStatus(final int status, final String message, final HttpStatus httpStatus) { + this.status = status; + this.message = message; + this.httpStatus = httpStatus; + } + + public int getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } +} diff --git a/src/main/java/racingcar/global/exception/response/DefaultExceptionResponse.java b/src/main/java/racingcar/global/exception/response/DefaultExceptionResponse.java new file mode 100644 index 000000000..ac45b9031 --- /dev/null +++ b/src/main/java/racingcar/global/exception/response/DefaultExceptionResponse.java @@ -0,0 +1,18 @@ +package racingcar.global.exception.response; + +public class DefaultExceptionResponse { + + private final String message; + + private DefaultExceptionResponse(final String message) { + this.message = message; + } + + public static DefaultExceptionResponse from(Exception exception) { + return new DefaultExceptionResponse(exception.getMessage()); + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/racingcar/global/exception/response/ExceptionResponse.java b/src/main/java/racingcar/global/exception/response/ExceptionResponse.java new file mode 100644 index 000000000..3e053f286 --- /dev/null +++ b/src/main/java/racingcar/global/exception/response/ExceptionResponse.java @@ -0,0 +1,52 @@ +package racingcar.global.exception.response; + +import org.springframework.validation.BindingResult; +import racingcar.global.exception.ExceptionStatus; + +import java.util.List; + +public class ExceptionResponse { + + private final int statusCode; + private final String message; + private final String status; + private final List errors; + + private ExceptionResponse(final int statusCode, final String message, + final String httpStatus, final List errors) { + this.statusCode = statusCode; + this.message = message; + this.status = httpStatus; + this.errors = errors; + } + + public static ExceptionResponse of(final ExceptionStatus exceptionStatus, BindingResult bindingResult) { + return new ExceptionResponse(exceptionStatus.getStatus(), + exceptionStatus.getMessage(), + exceptionStatus.getHttpStatus().name(), + ExceptionTargetInfoResponse.from(bindingResult)); + } + + public static ExceptionResponse of(final ExceptionStatus exceptionStatus) { + return new ExceptionResponse(exceptionStatus.getStatus(), + exceptionStatus.getMessage(), + exceptionStatus.getHttpStatus().name(), + null); + } + + public String getMessage() { + return message; + } + + public String getStatus() { + return status; + } + + public int getStatusCode() { + return statusCode; + } + + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/racingcar/global/exception/response/ExceptionTargetInfoResponse.java b/src/main/java/racingcar/global/exception/response/ExceptionTargetInfoResponse.java new file mode 100644 index 000000000..4ec285b04 --- /dev/null +++ b/src/main/java/racingcar/global/exception/response/ExceptionTargetInfoResponse.java @@ -0,0 +1,49 @@ +package racingcar.global.exception.response; + +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class ExceptionTargetInfoResponse { + + private static final String DEFAULT_ERROR_MESSAGE = ""; + + private final String field; + private final String value; + private final String reason; + + private ExceptionTargetInfoResponse(final String field, final String value, final String reason) { + this.field = field; + this.value = value; + this.reason = reason; + } + + public static List from(BindingResult bindingResult) { + + final List fieldErrors = bindingResult.getFieldErrors(); + + return fieldErrors.stream() + .map(error -> new ExceptionTargetInfoResponse( + error.getField(), + Objects.requireNonNullElse(error.getRejectedValue(), DEFAULT_ERROR_MESSAGE) + .toString(), + Objects.requireNonNullElse(error.getDefaultMessage(), DEFAULT_ERROR_MESSAGE)) + ) + .collect(Collectors.toList()); + } + + public String getField() { + return field; + } + + public String getValue() { + return value; + } + + public String getReason() { + return reason; + } +} diff --git a/src/main/java/racingcar/service/CarService.java b/src/main/java/racingcar/service/CarService.java index b717ec640..b5e0178fa 100644 --- a/src/main/java/racingcar/service/CarService.java +++ b/src/main/java/racingcar/service/CarService.java @@ -1,23 +1,32 @@ package racingcar.service; import org.springframework.stereotype.Service; -import racingcar.dao.car.CarDao; -import racingcar.dao.car.dto.CarRegisterRequest; -import racingcar.domain.Car; -import racingcar.domain.RacingCars; +import racingcar.dao.CarDao; +import racingcar.domain.RacingGame; +import racingcar.entity.CarEntity; +import racingcar.service.mapper.CarMapper; + +import java.util.List; @Service public class CarService { private final CarDao carDao; + private final CarMapper carMapper; - public CarService(final CarDao carDao) { + public CarService(final CarDao carDao, final CarMapper carMapper) { this.carDao = carDao; + this.carMapper = carMapper; + } + + public void registerCars(final RacingGame racingGame, final Long savedId) { + final List carEntities = + carMapper.mapToCarEntitiesFrom(racingGame, savedId); + + carDao.save(carEntities); } - public void registerCars(final RacingCars racingCars, final int savedId) { - for (final Car car : racingCars.getCars()) { - carDao.save(new CarRegisterRequest(car.getName(), car.getPosition(), savedId)); - } + public List searchAllCars() { + return carDao.findAll(); } } diff --git a/src/main/java/racingcar/service/RaceResultService.java b/src/main/java/racingcar/service/RaceResultService.java index 7b79dc0f6..dcdb8dce3 100644 --- a/src/main/java/racingcar/service/RaceResultService.java +++ b/src/main/java/racingcar/service/RaceResultService.java @@ -1,12 +1,13 @@ package racingcar.service; import org.springframework.stereotype.Service; -import racingcar.controller.dto.CarStatusResponse; -import racingcar.controller.dto.GameInfoRequest; -import racingcar.controller.dto.RaceResultResponse; -import racingcar.dao.raceresult.RaceResultDao; -import racingcar.dao.raceresult.dto.RaceResultRegisterRequest; -import racingcar.domain.RacingCars; +import org.springframework.transaction.annotation.Transactional; +import racingcar.dao.RaceResultDao; +import racingcar.domain.RacingGame; +import racingcar.entity.CarEntity; +import racingcar.entity.RaceResultEntity; +import racingcar.service.dto.GameInfoRequest; +import racingcar.service.dto.RaceResultResponse; import racingcar.service.mapper.RaceResultMapper; import racingcar.util.NumberGenerator; @@ -15,7 +16,6 @@ @Service public class RaceResultService { - private final RaceResultDao raceResultDao; private final CarService carService; private final NumberGenerator numberGenerator; @@ -29,24 +29,27 @@ public RaceResultService(final RaceResultDao raceResultDao, final CarService car this.raceResultMapper = raceResultMapper; } + @Transactional public RaceResultResponse createRaceResult(final GameInfoRequest gameInfoRequest) { - final String names = gameInfoRequest.getNames(); - final RacingCars racingCars = RacingCars.makeCars(names); - - final int tryCount = gameInfoRequest.getCount(); - - racingCars.moveAllCars(tryCount, numberGenerator); + final RacingGame racingGame = RacingGame.readyToRacingGame( + gameInfoRequest.getNames(), + numberGenerator, + gameInfoRequest.getCount() + ); + racingGame.play(); - final RaceResultRegisterRequest raceResultRegisterRequest = - raceResultMapper.mapToRaceResult(tryCount, racingCars); + final RaceResultEntity raceResultEntity = raceResultMapper.mapToRaceResultEntity(racingGame); + final Long savedId = raceResultDao.save(raceResultEntity); - final int savedId = raceResultDao.save(raceResultRegisterRequest); + carService.registerCars(racingGame, savedId); - carService.registerCars(racingCars, savedId); - - final List carStatusResponses = raceResultMapper.mapToCarStatus(racingCars); + return raceResultMapper.mapToRaceResultResponse(racingGame); + } - return new RaceResultResponse(raceResultRegisterRequest.getWinners(), carStatusResponses); + @Transactional(readOnly = true) + public List searchRaceResult() { + final List carEntities = carService.searchAllCars(); + return raceResultMapper.mapToRaceResultResponses(carEntities); } } diff --git a/src/main/java/racingcar/controller/dto/CarStatusResponse.java b/src/main/java/racingcar/service/dto/CarStatusResponse.java similarity index 91% rename from src/main/java/racingcar/controller/dto/CarStatusResponse.java rename to src/main/java/racingcar/service/dto/CarStatusResponse.java index 845e627b7..8385a9cf7 100644 --- a/src/main/java/racingcar/controller/dto/CarStatusResponse.java +++ b/src/main/java/racingcar/service/dto/CarStatusResponse.java @@ -1,4 +1,4 @@ -package racingcar.controller.dto; +package racingcar.service.dto; public class CarStatusResponse { diff --git a/src/main/java/racingcar/service/dto/GameInfoRequest.java b/src/main/java/racingcar/service/dto/GameInfoRequest.java new file mode 100644 index 000000000..8a7b88dca --- /dev/null +++ b/src/main/java/racingcar/service/dto/GameInfoRequest.java @@ -0,0 +1,29 @@ +package racingcar.service.dto; + +import javax.validation.constraints.Positive; +import javax.validation.constraints.Size; + +public class GameInfoRequest { + + @Size(min = 2) + private String names; + + @Positive + private int count; + + private GameInfoRequest() { + } + + public GameInfoRequest(final String names, final int count) { + this.names = names; + this.count = count; + } + + public String getNames() { + return names; + } + + public int getCount() { + return count; + } +} diff --git a/src/main/java/racingcar/controller/dto/RaceResultResponse.java b/src/main/java/racingcar/service/dto/RaceResultResponse.java similarity index 93% rename from src/main/java/racingcar/controller/dto/RaceResultResponse.java rename to src/main/java/racingcar/service/dto/RaceResultResponse.java index b29eb24c3..60a4a10b9 100644 --- a/src/main/java/racingcar/controller/dto/RaceResultResponse.java +++ b/src/main/java/racingcar/service/dto/RaceResultResponse.java @@ -1,4 +1,4 @@ -package racingcar.controller.dto; +package racingcar.service.dto; import java.util.List; diff --git a/src/main/java/racingcar/service/mapper/CarMapper.java b/src/main/java/racingcar/service/mapper/CarMapper.java new file mode 100644 index 000000000..51c86eaf3 --- /dev/null +++ b/src/main/java/racingcar/service/mapper/CarMapper.java @@ -0,0 +1,34 @@ +package racingcar.service.mapper; + +import org.springframework.stereotype.Component; +import racingcar.domain.Car; +import racingcar.domain.RacingGame; +import racingcar.entity.CarEntity; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class CarMapper { + + public List mapToCarEntitiesFrom(final RacingGame racingGame, + final Long savedId) { + return racingGame.getParticipantAllCar() + .stream() + .map(it -> mapToCarEntityFrom(racingGame, savedId, it)) + .collect(Collectors.toList()); + } + + private static CarEntity mapToCarEntityFrom(final RacingGame racingGame, + final Long savedId, + final Car car) { + return new CarEntity( + car.getName(), + car.getPosition(), + savedId, + racingGame.isWinner(car), + LocalDateTime.now() + ); + } +} diff --git a/src/main/java/racingcar/service/mapper/RaceResultMapper.java b/src/main/java/racingcar/service/mapper/RaceResultMapper.java index 5147f6bbc..283a7fa58 100644 --- a/src/main/java/racingcar/service/mapper/RaceResultMapper.java +++ b/src/main/java/racingcar/service/mapper/RaceResultMapper.java @@ -1,36 +1,78 @@ package racingcar.service.mapper; import org.springframework.stereotype.Component; -import racingcar.controller.dto.CarStatusResponse; -import racingcar.dao.raceresult.dto.RaceResultRegisterRequest; import racingcar.domain.Car; -import racingcar.domain.RacingCars; +import racingcar.domain.RacingGame; +import racingcar.entity.CarEntity; +import racingcar.entity.RaceResultEntity; +import racingcar.service.dto.CarStatusResponse; +import racingcar.service.dto.RaceResultResponse; +import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import static java.util.stream.Collectors.toList; + @Component public class RaceResultMapper { private static final String CAR_NAMES_DELIMITER = ","; - public RaceResultRegisterRequest mapToRaceResult(final int tryCount, final RacingCars racingCars) { - return new RaceResultRegisterRequest(tryCount, - String.join(CAR_NAMES_DELIMITER, mapToNameFrom(racingCars))); + public RaceResultEntity mapToRaceResultEntity(final RacingGame racingGame) { + return new RaceResultEntity(racingGame.getTrialCount(), + LocalDateTime.now()); } - private List mapToNameFrom(RacingCars racingCars) { - return racingCars.getWinners() - .stream() - .map(Car::getName) - .collect(Collectors.toList()); + public RaceResultResponse mapToRaceResultResponse(final RacingGame racingGame) { + final List carStatusResponses = mapToCarStatusResponseFrom(racingGame); + final String winners = mapToWinnersNameFrom(racingGame); + + return new RaceResultResponse(winners, carStatusResponses); } - public List mapToCarStatus(final RacingCars racingCars) { - return racingCars.getCars() + private List mapToCarStatusResponseFrom(final RacingGame racingGame) { + return racingGame.getParticipantAllCar() .stream() .map(car -> new CarStatusResponse(car.getName(), car.getPosition())) - .collect(Collectors.toList()); + .collect(toList()); + } + + private String mapToWinnersNameFrom(final RacingGame racingGame) { + return racingGame.determineWinners() + .stream() + .map(Car::getName) + .collect(Collectors.joining(",")); + } + + public List mapToRaceResultResponses(final List carEntities) { + final Map> groupingByRaceResultId = + carEntities.stream() + .collect(Collectors.groupingBy( + CarEntity::getRaceResultId) + ); + + return groupingByRaceResultId.values() + .stream() + .map(it -> new RaceResultResponse( + mapToWinnersNameFrom(it), + mapToCarStatusResponses(it)) + ) + .collect(toList()); + } + + private String mapToWinnersNameFrom(final List carEntities) { + return carEntities.stream() + .filter(CarEntity::isWinner) + .map(CarEntity::getName) + .collect(Collectors.joining(CAR_NAMES_DELIMITER)); + } + + private List mapToCarStatusResponses(final List carEntities) { + return carEntities.stream() + .map(it -> new CarStatusResponse(it.getName(), it.getPosition())) + .collect(toList()); } } diff --git a/src/main/java/racingcar/util/RandomNumberGenerator.java b/src/main/java/racingcar/util/RandomNumberGenerator.java index a6fbe9be0..f24cb036d 100644 --- a/src/main/java/racingcar/util/RandomNumberGenerator.java +++ b/src/main/java/racingcar/util/RandomNumberGenerator.java @@ -2,16 +2,17 @@ import org.springframework.stereotype.Component; -import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; @Component public class RandomNumberGenerator implements NumberGenerator { private static final int MAX_RANDOM_INT = 10; + private static final ThreadLocalRandom random = ThreadLocalRandom.current(); + @Override public int generate() { - Random random = new Random(); return random.nextInt(MAX_RANDOM_INT); } } diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java index 8f649316a..23c7a043e 100644 --- a/src/main/java/racingcar/view/InputView.java +++ b/src/main/java/racingcar/view/InputView.java @@ -4,22 +4,11 @@ public class InputView { - private static final String CAR_NAMES_DELIMITER = ","; - private final InputViewValidator inputViewValidator = new InputViewValidator(); - public String[] readCarNames() { + public String readCarNames() { Scanner scanner = new Scanner(System.in); - String carNames = scanner.nextLine(); - inputViewValidator.validateCarNames(carNames); - String[] splitCarNames = getSplitCarNames(carNames); - inputViewValidator.validateSplitCarNames(splitCarNames); - - return splitCarNames; - } - - private String[] getSplitCarNames(String carNames) { - return carNames.split(CAR_NAMES_DELIMITER, -1); + return scanner.nextLine(); } public int readTryNum() { diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java index 75e52524c..c63737c1c 100644 --- a/src/main/java/racingcar/view/OutputView.java +++ b/src/main/java/racingcar/view/OutputView.java @@ -1,31 +1,21 @@ package racingcar.view; -import java.util.List; +import racingcar.domain.Car; +import racingcar.domain.RacingCars; + import java.util.Map; +import java.util.stream.Collectors; public class OutputView { - private static final String PRINT_READ_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."; - private static final String PRINT_READ_TRY_NUM_MESSAGE = "시도할 회수는 몇회인가요?"; - private static final String PRINT_RACING_RESULT_MESSAGE = "\n실행 결과"; - private static final String PRINT_WINNERS_MESSAGE = "가 최종 우승했습니다."; + private static final String PRINT_CAR_NAME = "자동차 이름 : "; + private static final String PRINT_POSITION_SIZE = "이동한 거리 : "; + private static final String PRINT_WINNER = "우승자 : "; private void printMessage(String message) { System.out.println(message); } - public void printReadCarNamesMessage() { - printMessage(PRINT_READ_CAR_NAMES_MESSAGE); - } - - public void printReadTryNumMessage() { - printMessage(PRINT_READ_TRY_NUM_MESSAGE); - } - - public void printRacingResultMessage() { - printMessage(PRINT_RACING_RESULT_MESSAGE); - } - public void printCurrentRacingCarsPosition(Map carPositonMap) { for (String carName : carPositonMap.keySet()) { Integer position = carPositonMap.get(carName); @@ -35,7 +25,20 @@ public void printCurrentRacingCarsPosition(Map carPositonMap) { printMessage(""); } - public void printWinners(List winners) { - printMessage(String.join(", ", winners) + PRINT_WINNERS_MESSAGE); + public void printWinners(RacingCars racingCars) { + final String winners = racingCars.getWinners() + .stream() + .map(Car::getName) + .collect(Collectors.joining(",")); + + System.out.println(PRINT_WINNER + winners); + + racingCars.getCars() + .forEach(car -> System.out.println( + PRINT_CAR_NAME + + car.getName() + + PRINT_POSITION_SIZE + + car.getPosition()) + ); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3fdeef6a3..4848364f3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,3 +5,7 @@ spring: datasource: url: jdbc:h2:mem:testdb;MODE=MySQL driver-class-name: org.h2.Driver + sql: + init: + schema-locations: classpath:sql/schema.sql + data-locations: classpath:sql/data.sql diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index 0cdb1dad1..000000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE RACE_RESULT -( - id INT NOT NULL AUTO_INCREMENT, - trial_count INT NOT NULL, - winners VARCHAR(255) NOT NULL, - created_at DATETIME NOT NULL, - PRIMARY KEY (id) -); - -CREATE TABLE CAR -( - id INT NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL, - position INT NOT NULL, - play_result_id INT NOT NULL, - PRIMARY KEY (id) -); diff --git a/src/main/resources/sql/data.sql b/src/main/resources/sql/data.sql new file mode 100644 index 000000000..c153995b6 --- /dev/null +++ b/src/main/resources/sql/data.sql @@ -0,0 +1,17 @@ +insert into RACE_RESULT +values (1, 10, current_timestamp); +insert into CAR +values (1, '우르', 9, 1, 1, current_timestamp); +insert into CAR +values (2, '빙봉', 10, 1, 0, current_timestamp); + +insert into RACE_RESULT +values (2, 5, current_timestamp); +insert into CAR +values (3, 'a', 5, 2, 1, current_timestamp); +insert into CAR +values (4, 'b', 5, 2, 1, current_timestamp); +insert into CAR +values (5, 'c', 5, 2, 1, current_timestamp); +insert into CAR +values (6, 'd', 5, 2, 1, current_timestamp); diff --git a/src/main/resources/sql/schema.sql b/src/main/resources/sql/schema.sql new file mode 100644 index 000000000..d397cdf6b --- /dev/null +++ b/src/main/resources/sql/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE RACE_RESULT +( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + trial_count INT NOT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE CAR +( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + position INT NOT NULL, + race_result_id BIGINT NOT NULL, + winner BOOL NOT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id) +); diff --git a/src/test/java/racingcar/controller/RacingCarWebControllerIntegrationTest.java b/src/test/java/racingcar/controller/RacingCarWebControllerIntegrationTest.java new file mode 100644 index 000000000..a521c8cfe --- /dev/null +++ b/src/test/java/racingcar/controller/RacingCarWebControllerIntegrationTest.java @@ -0,0 +1,58 @@ +package racingcar.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import racingcar.global.exception.ExceptionStatus; +import racingcar.service.RaceResultService; +import racingcar.service.dto.GameInfoRequest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(RacingCarWebController.class) +class RacingCarWebControllerIntegrationTest { + + @MockBean + private RaceResultService raceResultService; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Test + @DisplayName("데이터베이스 연결 실패 테스트") + void test_registerRaceResult_connectionFailure() throws Exception { + //given + final GameInfoRequest gameInfoRequest = new GameInfoRequest("a,b,c,d", 4); + final String requestData = objectMapper.writeValueAsString(gameInfoRequest); + + //when + when(raceResultService.createRaceResult(any())) + .thenThrow(DataAccessResourceFailureException.class); + + //then + mockMvc.perform(post("/plays") + .contentType(MediaType.APPLICATION_JSON) + .content(requestData)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.statusCode").value( + ExceptionStatus.TEMPORARY_DATABASE_CONNECTION_EXCEPTION.getStatus() + ) + ) + .andExpect(jsonPath("$.message").value( + ExceptionStatus.TEMPORARY_DATABASE_CONNECTION_EXCEPTION.getMessage()) + ); + } +} diff --git a/src/test/java/racingcar/controller/RacingCarWebControllerTest.java b/src/test/java/racingcar/controller/RacingCarWebControllerTest.java new file mode 100644 index 000000000..40f86b599 --- /dev/null +++ b/src/test/java/racingcar/controller/RacingCarWebControllerTest.java @@ -0,0 +1,106 @@ +package racingcar.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import racingcar.service.RaceResultService; +import racingcar.service.dto.CarStatusResponse; +import racingcar.service.dto.GameInfoRequest; +import racingcar.service.dto.RaceResultResponse; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("RacingCarWebController Unit Test") +class RacingCarWebControllerTest { + + private MockMvc mockMvc; + + private RacingCarWebController racingCarWebController; + + private RaceResultService raceResultService; + + private ObjectMapper objectMapper; + + @BeforeEach + void init() { + objectMapper = new ObjectMapper(); + raceResultService = mock(RaceResultService.class); + racingCarWebController = new RacingCarWebController(raceResultService); + + mockMvc = MockMvcBuilders.standaloneSetup(racingCarWebController) + .build(); + } + + @Test + @DisplayName("registerRaceResult() : 게임 정보를 통해 새로운 게임을 생성할 수 있다.") + void test_registerRaceResult() throws Exception { + //given + final GameInfoRequest gameInfoRequest = new GameInfoRequest("a,b,c,d", 4); + final List carStatusResponses = List.of(new CarStatusResponse("a", 4), + new CarStatusResponse("b", 2), + new CarStatusResponse("c", 4), + new CarStatusResponse("d", 3)); + + final String bodyData = objectMapper.writeValueAsString(gameInfoRequest); + + //when + final RaceResultResponse raceResultResponse = new RaceResultResponse("a,c", carStatusResponses); + + when(raceResultService.createRaceResult(any())) + .thenReturn(raceResultResponse); + + //then + + final String resultData = objectMapper.writeValueAsString(raceResultResponse); + + mockMvc.perform(post("/plays") + .contentType(MediaType.APPLICATION_JSON) + .content(bodyData)) + .andExpect(status().isCreated()) + .andExpect(content().json(resultData)); + } + + @Test + @DisplayName("showRaceResult() : 모든 게임 정보를 불러올 수 있다.") + void test_showRaceResult() throws Exception { + //given + final List carStatusResponses1 = List.of(new CarStatusResponse("a", 4), + new CarStatusResponse("b", 2), + new CarStatusResponse("c", 4), + new CarStatusResponse("d", 3)); + + final List carStatusResponses2 = List.of(new CarStatusResponse("e", 4), + new CarStatusResponse("f", 2), + new CarStatusResponse("g", 4), + new CarStatusResponse("h", 3)); + + final List raceResultResponses = + List.of(new RaceResultResponse("a,c", carStatusResponses1), + new RaceResultResponse("e,g", carStatusResponses2)); + + //when + when(raceResultService.searchRaceResult()) + .thenReturn(raceResultResponses); + + //then + + final String resultData = objectMapper.writeValueAsString(raceResultResponses); + + mockMvc.perform(get("/plays") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().json(resultData)); + } +} diff --git a/src/test/java/racingcar/dao/CarDaoTest.java b/src/test/java/racingcar/dao/CarDaoTest.java new file mode 100644 index 000000000..2a89cab04 --- /dev/null +++ b/src/test/java/racingcar/dao/CarDaoTest.java @@ -0,0 +1,53 @@ +package racingcar.dao; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import racingcar.entity.CarEntity; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JdbcTest +class CarDaoTest { + + CarDao carDao; + + @Autowired + JdbcTemplate jdbcTemplate; + + @BeforeEach + void init() { + carDao = new CarDao(jdbcTemplate); + } + + @Test + @DisplayName("CarEntity를 통해서 Car 를 저장할 수 있다.") + void test_save() throws Exception { + //given + final List carEntities = + List.of( + new CarEntity("a", 1, 3L, true, LocalDateTime.now()), + new CarEntity("b", 2, 3L, true, LocalDateTime.now()) + ); + + //when + Assertions.assertDoesNotThrow(() -> carDao.save(carEntities)); + } + + @Test + @DisplayName("자동차들을 전부 조회할 수 있다.") + void test_findCarsBy() throws Exception { + //when + final List carEntities = carDao.findAll(); + + //then + assertEquals(6, carEntities.size()); + } +} diff --git a/src/test/java/racingcar/dao/RaceResultDaoTest.java b/src/test/java/racingcar/dao/RaceResultDaoTest.java new file mode 100644 index 000000000..bb7548ec1 --- /dev/null +++ b/src/test/java/racingcar/dao/RaceResultDaoTest.java @@ -0,0 +1,40 @@ +package racingcar.dao; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import racingcar.entity.RaceResultEntity; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JdbcTest +class RaceResultDaoTest { + + RaceResultDao raceResultDao; + + @Autowired + JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + raceResultDao = new RaceResultDao(jdbcTemplate); + } + + @Test + @DisplayName("save() : 경기 결과를 저장할 수 있다.") + void test_save() throws Exception { + //given + final RaceResultEntity raceResultEntity = new RaceResultEntity(4, LocalDateTime.now()); + + //when + final Long savedId = raceResultDao.save(raceResultEntity); + + //then + assertEquals(1L, savedId); + } +} diff --git a/src/test/java/racingcar/domain/CarTest.java b/src/test/java/racingcar/domain/CarTest.java index 2def91d9a..4dcf92fa1 100644 --- a/src/test/java/racingcar/domain/CarTest.java +++ b/src/test/java/racingcar/domain/CarTest.java @@ -1,13 +1,14 @@ package racingcar.domain; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + class CarTest { Car car; @@ -23,7 +24,7 @@ void generateCar() { @DisplayName("자동차 이름이 5글자 초과인 경우") void wrongCarNameLengthTest(String carName) { - Assertions.assertThatThrownBy(() -> new Car(carName, 0)) + assertThatThrownBy(() -> new Car(carName, 0)) .isInstanceOf(IllegalArgumentException.class); } @@ -32,7 +33,7 @@ void wrongCarNameLengthTest(String carName) { void carNameBlankTest() { String carName = ""; - Assertions.assertThatThrownBy(() -> new Car(carName, 0)) + assertThatThrownBy(() -> new Car(carName, 0)) .isInstanceOf(IllegalArgumentException.class); } @@ -42,7 +43,7 @@ void carNotMoveTest() { int beforePosition = car.getPosition(); car.move(3); - Assertions.assertThat(car.getPosition()).isEqualTo(beforePosition); + assertThat(car.getPosition()).isEqualTo(beforePosition); } @Test @@ -51,7 +52,7 @@ void carMoveTest() { int beforePosition = car.getPosition(); car.move(4); - Assertions.assertThat(car.getPosition()).isEqualTo(beforePosition + 1); + assertThat(car.getPosition()).isEqualTo(beforePosition + 1); } @Test @@ -59,7 +60,7 @@ void carMoveTest() { void getLargerCarTest() { compareCar = new Car("car", 5); - Assertions.assertThat(car.compareTo(compareCar)).isLessThan(0); + assertThat(car.compareTo(compareCar)).isLessThan(0); } @Test @@ -67,7 +68,7 @@ void getLargerCarTest() { void getSamePositionCarExistTest() { compareCar = new Car("car", 0); - Assertions.assertThat(car.isSamePositionCar(compareCar)).isTrue(); + assertThat(car.isSamePositionCar(compareCar)).isTrue(); } @Test @@ -75,6 +76,6 @@ void getSamePositionCarExistTest() { void getSamePositionCarNotExistTest() { compareCar = new Car("car", 5); - Assertions.assertThat(car.isSamePositionCar(compareCar)).isFalse(); + assertThat(car.isSamePositionCar(compareCar)).isFalse(); } } diff --git a/src/test/java/racingcar/domain/RacingCarsTest.java b/src/test/java/racingcar/domain/RacingCarsTest.java index 5cc27734f..3a426e705 100644 --- a/src/test/java/racingcar/domain/RacingCarsTest.java +++ b/src/test/java/racingcar/domain/RacingCarsTest.java @@ -1,13 +1,11 @@ package racingcar.domain; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -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.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + class RacingCarsTest { @@ -19,8 +17,8 @@ void getOneWinnersTest() { final Car test1 = racingCars.getCars().get(0); test1.move(4); - Assertions.assertThat(racingCars.getWinners().size()).isEqualTo(1); - Assertions.assertThat(racingCars.getWinners()).containsExactly(test1); + assertThat(racingCars.getWinners().size()).isEqualTo(1); + assertThat(racingCars.getWinners()).containsExactly(test1); } @Test @@ -36,8 +34,8 @@ void getMultipleWinnersTest() { test2.move(4); test3.move(4); - Assertions.assertThat(racingCars.getWinners().size()).isEqualTo(3); - Assertions.assertThat(racingCars.getWinners()).containsOnly(test1, test2, test3); + assertThat(racingCars.getWinners().size()).isEqualTo(3); + assertThat(racingCars.getWinners()).containsOnly(test1, test2, test3); } @@ -58,8 +56,8 @@ void getAllWinnersTest() { test4.move(4); test5.move(4); - Assertions.assertThat(racingCars.getWinners().size()).isEqualTo(5); - Assertions.assertThat(racingCars.getWinners()).containsOnly(test1, test2, test3, test4, test5); + assertThat(racingCars.getWinners().size()).isEqualTo(5); + assertThat(racingCars.getWinners()).containsOnly(test1, test2, test3, test4, test5); } @Test @@ -67,7 +65,7 @@ void getAllWinnersTest() { void splitCarNamesDuplicateTest() { String carNames = "성하,성하,이오"; - Assertions.assertThatThrownBy(() -> RacingCars.makeCars(carNames)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> RacingCars.makeCars(carNames)) + .isInstanceOf(IllegalArgumentException.class); } } diff --git a/src/test/java/racingcar/service/CarServiceIntegrationTest.java b/src/test/java/racingcar/service/CarServiceIntegrationTest.java new file mode 100644 index 000000000..92a88ab32 --- /dev/null +++ b/src/test/java/racingcar/service/CarServiceIntegrationTest.java @@ -0,0 +1,33 @@ +package racingcar.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import racingcar.domain.RacingGame; +import racingcar.util.RandomNumberGenerator; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@DisplayName("CarService Integration Test") +@SpringBootTest +class CarServiceIntegrationTest { + + @Autowired + private CarService carService; + + @Test + @DisplayName("registerCars() : 자동차들을 저장합니다.") + void test_registerCars() throws Exception { + //given + final int trialCount = 2; + final RacingGame racingGame = + RacingGame.readyToRacingGame("a,b,c", + new RandomNumberGenerator(), + trialCount); + final Long savedRaceResultId = 3L; + + //when & then + assertDoesNotThrow(() -> carService.registerCars(racingGame, savedRaceResultId)); + } +} diff --git a/src/test/java/racingcar/service/CarServiceTest.java b/src/test/java/racingcar/service/CarServiceTest.java new file mode 100644 index 000000000..d2fdc7f77 --- /dev/null +++ b/src/test/java/racingcar/service/CarServiceTest.java @@ -0,0 +1,63 @@ +package racingcar.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.dao.CarDao; +import racingcar.domain.RacingGame; +import racingcar.entity.CarEntity; +import racingcar.service.mapper.CarMapper; +import racingcar.util.RandomNumberGenerator; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@DisplayName("CarService Unit Test") +class CarServiceTest { + + private CarService carService; + + private CarDao carDao; + private CarMapper carMapper; + + @BeforeEach + void init() { + carDao = mock(CarDao.class); + carMapper = mock(CarMapper.class); + + carService = new CarService(carDao, carMapper); + } + + @Test + @DisplayName("registerCars() : 자동차들을 저장합니다.") + void test_registerCars() throws Exception { + //given + final RacingGame racingGame = + RacingGame.readyToRacingGame("a,b,c", + new RandomNumberGenerator(), + 2); + final Long savedRaceResultId = 1L; + + final List carEntities = List.of( + new CarEntity("a", 3, 1L, true, LocalDateTime.now()), + new CarEntity("b", 3, 1L, true, LocalDateTime.now()), + new CarEntity("c", 3, 1L, true, LocalDateTime.now()) + ); + + //when + when(carMapper.mapToCarEntitiesFrom(any(), anyLong())) + .thenReturn(carEntities); + + carService.registerCars(racingGame, savedRaceResultId); + + //then + verify(carDao, times(1)).save(any()); + } +} diff --git a/src/test/java/racingcar/service/RaceResultServiceIntegrationTest.java b/src/test/java/racingcar/service/RaceResultServiceIntegrationTest.java new file mode 100644 index 000000000..d04242e42 --- /dev/null +++ b/src/test/java/racingcar/service/RaceResultServiceIntegrationTest.java @@ -0,0 +1,56 @@ +package racingcar.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import racingcar.service.dto.CarStatusResponse; +import racingcar.service.dto.GameInfoRequest; +import racingcar.service.dto.RaceResultResponse; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@DisplayName("RaceResultService Integration Test") +class RaceResultServiceIntegrationTest { + + @Autowired + private RaceResultService raceResultService; + + @Test + @DisplayName("createRaceResult() : 게임 정보를 통해 새로운 게임을 만들 수 있다.") + void test_createRaceResult() throws Exception { + //given + final String input = "a,b,c,d"; + final List splitInput = List.of(input.split(",")); + + final GameInfoRequest gameInfoRequest = new GameInfoRequest(input, splitInput.size()); + + //when + final RaceResultResponse raceResult = raceResultService.createRaceResult(gameInfoRequest); + + //then + final List racingCars = raceResult.getRacingCars(); + + assertThat(racingCars).hasSize(splitInput.size()) + .extracting("name") + .containsExactlyElementsOf(splitInput); + } + + @Test + @DisplayName("searchRaceResult() : 모든 경기 결과를 조회할 수 있다.") + void test_searchRaceResult() throws Exception { + //given + final List winnerResult = List.of("빙봉", "a,b,c,d"); + + //when + final List raceResultResponses = raceResultService.searchRaceResult(); + + //then + assertThat(raceResultResponses).extracting("winners") + .hasSize(winnerResult.size()) + .containsExactlyElementsOf(winnerResult); + } +} diff --git a/src/test/java/racingcar/service/RaceResultServiceTest.java b/src/test/java/racingcar/service/RaceResultServiceTest.java new file mode 100644 index 000000000..7e3460fa8 --- /dev/null +++ b/src/test/java/racingcar/service/RaceResultServiceTest.java @@ -0,0 +1,141 @@ +package racingcar.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import racingcar.dao.RaceResultDao; +import racingcar.entity.CarEntity; +import racingcar.entity.RaceResultEntity; +import racingcar.service.dto.CarStatusResponse; +import racingcar.service.dto.GameInfoRequest; +import racingcar.service.dto.RaceResultResponse; +import racingcar.service.mapper.RaceResultMapper; +import racingcar.util.NumberGenerator; +import racingcar.util.RandomNumberGenerator; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@DisplayName("RaceResultService Unit Test") +class RaceResultServiceTest { + + private RaceResultMapper raceResultMapper; + private CarService carService; + private RaceResultDao raceResultDao; + private NumberGenerator numberGenerator; + + private RaceResultService raceResultService; + + @BeforeEach + void dataInit() { + carService = mock(CarService.class); + raceResultMapper = mock(RaceResultMapper.class); + raceResultDao = mock(RaceResultDao.class); + numberGenerator = new RandomNumberGenerator(); + + raceResultService = + new RaceResultService( + raceResultDao, carService, + numberGenerator, raceResultMapper + ); + } + + @Test + @DisplayName("createRaceResult() : 게임 정보를 통해 새로운 게임을 만들 수 있다.") + void test_createRaceResult() throws Exception { + //given + final GameInfoRequest gameInfoRequest = new GameInfoRequest("a,b,c,d", 4); + final int trialCount = gameInfoRequest.getCount(); + + final RaceResultEntity raceResultEntity = + new RaceResultEntity(trialCount, LocalDateTime.now()); + + final List carStatusResponses = + List.of(new CarStatusResponse("a", 1), + new CarStatusResponse("b", 1), + new CarStatusResponse("c", 1), + new CarStatusResponse("d", 0)); + + final RaceResultResponse raceResultResponse = new RaceResultResponse("a,b,c", carStatusResponses); + + //when + when(raceResultMapper.mapToRaceResultEntity(any())) + .thenReturn(raceResultEntity); + + when(raceResultDao.save(any())) + .thenReturn(1L); + + doNothing().when(carService) + .registerCars(any(), anyLong()); + + when(raceResultMapper.mapToRaceResultResponse(any())) + .thenReturn(raceResultResponse); + + final RaceResultResponse raceResult = raceResultService.createRaceResult(gameInfoRequest); + + //then + final List carStatusResult = raceResult.getRacingCars(); + + assertAll( + () -> assertEquals("a,b,c", raceResult.getWinners()), + () -> assertThat(carStatusResult).hasSize(4), + () -> assertThat(carStatusResult).extracting("name") + .containsExactly("a", "b", "c", "d"), + () -> assertThat(carStatusResult).extracting("position") + .containsExactly(1, 1, 1, 0) + ); + } + + @Test + @DisplayName("searchRaceResult() : 모든 경기 결과를 조회할 수 있다.") + void test_searchRaceResult() throws Exception { + //given + final List cars = + List.of(new CarEntity("a", 2, 1L, true, LocalDateTime.now()), + new CarEntity("b", 3, 1L, true, LocalDateTime.now()), + new CarEntity("c", 1, 1L, true, LocalDateTime.now()), + new CarEntity("d", 4, 1L, true, LocalDateTime.now())); + + final List carStatusResponses = List.of(new CarStatusResponse("a", 2), + new CarStatusResponse("b", 3), + new CarStatusResponse("c", 1), + new CarStatusResponse("d", 4)); + + final List raceResultResponses = List.of( + new RaceResultResponse("d", carStatusResponses)); + + when(carService.searchAllCars()) + .thenReturn(cars); + + when(raceResultMapper.mapToRaceResultResponses(cars)) + .thenReturn(raceResultResponses); + + final List result = raceResultService.searchRaceResult(); + + //then + final RaceResultResponse response = result.get(0); + + final Map responseMap = response.getRacingCars() + .stream() + .collect(Collectors.toMap(CarStatusResponse::getName, + CarStatusResponse::getPosition)); + + assertAll( + () -> assertEquals("d", response.getWinners()), + () -> assertThat(response.getRacingCars()).hasSize(4), + () -> assertThat(responseMap).containsKeys("a", "b", "c", "d") + .containsValues(2, 3, 1, 4) + ); + } +} diff --git a/src/test/java/racingcar/view/InputViewValidatorTest.java b/src/test/java/racingcar/view/InputViewValidatorTest.java index df4209881..7d03188f4 100644 --- a/src/test/java/racingcar/view/InputViewValidatorTest.java +++ b/src/test/java/racingcar/view/InputViewValidatorTest.java @@ -1,12 +1,13 @@ package racingcar.view; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + class InputViewValidatorTest { private final InputViewValidator inputViewValidator = new InputViewValidator(); @@ -20,7 +21,7 @@ class carNamesTest { void carNamesBlankTest() { String carNames = ""; - Assertions.assertThatThrownBy(() -> inputViewValidator.validateCarNames(carNames)) + assertThatThrownBy(() -> inputViewValidator.validateCarNames(carNames)) .isInstanceOf(IllegalArgumentException.class); } } @@ -34,7 +35,7 @@ class splitCarNameTest { void splitCarNamesDuplicateTest() { String[] carNames = new String[]{"성하", "성하", "이오"}; - Assertions.assertThatThrownBy(() -> inputViewValidator.validateSplitCarNames(carNames)) + assertThatThrownBy(() -> inputViewValidator.validateSplitCarNames(carNames)) .isInstanceOf(IllegalArgumentException.class); } } @@ -48,7 +49,7 @@ class tryNumTest { void tryNumBlankTest() { String tryNum = ""; - Assertions.assertThatThrownBy(() -> inputViewValidator.validateTryNum(tryNum)) + assertThatThrownBy(() -> inputViewValidator.validateTryNum(tryNum)) .isInstanceOf(IllegalArgumentException.class); } @@ -57,7 +58,7 @@ void tryNumBlankTest() { @DisplayName("시도할 횟수가 정수가 아닌 경우 예외 처리") void tryNumIntegerTest(String tryNum) { - Assertions.assertThatThrownBy(() -> inputViewValidator.validateTryNum(tryNum)) + assertThatThrownBy(() -> inputViewValidator.validateTryNum(tryNum)) .isInstanceOf(IllegalArgumentException.class); } @@ -66,7 +67,7 @@ void tryNumIntegerTest(String tryNum) { @DisplayName("시도할 횟수가 0 이하인 경우 예외 테스트") void readTryNumNotPositiveTest(String tryNum) { - Assertions.assertThatThrownBy(() -> inputViewValidator.validateTryNum(tryNum)) + assertThatThrownBy(() -> inputViewValidator.validateTryNum(tryNum)) .isInstanceOf(IllegalArgumentException.class); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 000000000..3f9756363 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,11 @@ +spring: + h2: + console: + enabled: true + datasource: + url: jdbc:h2:mem:test;MODE=MySQL + driver-class-name: org.h2.Driver + sql: + init: + schema-locations: classpath:sql/schema.sql + data-locations: classpath:sql/data.sql diff --git a/src/test/resources/sql/data.sql b/src/test/resources/sql/data.sql new file mode 100644 index 000000000..e5698261e --- /dev/null +++ b/src/test/resources/sql/data.sql @@ -0,0 +1,17 @@ +insert into RACE_RESULT +values (3, 10, current_timestamp); +insert into CAR +values (4, '우르', 9, 1, 0, current_timestamp); +insert into CAR +values (5, '빙봉', 10, 1, 1, current_timestamp); + +insert into RACE_RESULT +values (4, 5, current_timestamp); +insert into CAR +values (6, 'a', 5, 2, 1, current_timestamp); +insert into CAR +values (7, 'b', 5, 2, 1, current_timestamp); +insert into CAR +values (8, 'c', 5, 2, 1, current_timestamp); +insert into CAR +values (9, 'd', 5, 2, 1, current_timestamp); diff --git a/src/test/resources/sql/schema.sql b/src/test/resources/sql/schema.sql new file mode 100644 index 000000000..61215b7ae --- /dev/null +++ b/src/test/resources/sql/schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE RACE_RESULT +( + id BIGINT NOT NULL AUTO_INCREMENT, + trial_count INT NOT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE CAR +( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + position INT NOT NULL, + race_result_id BIGINT NOT NULL, + winner BOOL NOT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id) +);