Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0a25a5a
<CHORE> README.md 에 기능 목록 생성
Long9725 Jun 9, 2024
d91971f
<TEST> Racer 생성자 테스트
Long9725 Jun 9, 2024
40df0cd
<FEAT> Racer 생성자
Long9725 Jun 9, 2024
dd442db
<FEAT> Racer 생성자 Exception 테스트
Long9725 Jun 9, 2024
3ef0970
<REFACTOR> Racer 생성자에 validate 로직 추가
Long9725 Jun 9, 2024
e0750f1
<TEST> Racer moveIfCan 메소드 테스트
Long9725 Jun 9, 2024
118eeac
<FEAT> Racer moveIfCan 메소드 구현
Long9725 Jun 9, 2024
7de8fe5
<CHORE> Update README.md
Long9725 Jun 9, 2024
ea71120
<TEST> Racer isWinner 메소드 테스트
Long9725 Jun 9, 2024
90d5145
<FEAT> Racer isWinner 메소드 구현
Long9725 Jun 9, 2024
63e9c2c
<REFACTOR> Racer 생성자 javadoc 추가
Long9725 Jun 9, 2024
be79e4f
<TEST> RacerDto of 메소드 테스트 추가
Long9725 Jun 9, 2024
10c5092
<FEAT> RacerDto of 메소드 구현 추가
Long9725 Jun 9, 2024
55d3a45
<TEST> RacerController setUp 메소드 테스트 추가
Long9725 Jun 9, 2024
44f830a
<FEAT> RacerController setUpRacer 메소드 구현
Long9725 Jun 9, 2024
40c3f95
<TEST> RacerController setUpRacer 메소드 실패 테스트 추가
Long9725 Jun 9, 2024
86e8aee
<TEST> RaceController setUpGameCount 메소드 테스트 추가
Long9725 Jun 9, 2024
fd3ffdc
<FEAT> RacerController setUpGameCount 메소드 구현
Long9725 Jun 9, 2024
02fe2f5
<CHORE> Update README.md
Long9725 Jun 9, 2024
24ec9d2
<REFACTOR> Racer 이름 조건에 5자 이하 문자열 검증 추가
Long9725 Jun 9, 2024
2dfe34b
<TEST> RaceController playGame 메소드 테스트 추가
Long9725 Jun 9, 2024
6ffccbb
<FEAT> RacerController playGame 메소드 구현
Long9725 Jun 9, 2024
4b24168
<FEAT> RacerView
Long9725 Jun 9, 2024
f02a024
<CHORE> Update README.md
Long9725 Jun 9, 2024
0b2e21b
<CHORE> Update README.md
Long9725 Jun 9, 2024
38c9804
<CHORE> Update README.md
Long9725 Jun 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions QUESTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 질문 모음

+ 입력값 검증에 대한 테스트 코드를 짤 때, 잘못된 입력값 여러 개를 테스트 하고 싶다면 주로 어떻게 하는 것이 좋은지?

아래처럼 배열로 하는 것이 좋은건지, 아니면 다른 더 좋은 방법이 있는건지?!
```java
class RacerTest {
@Test
@DisplayName("Racer 생성자 실패 테스트")
void racerConstructorWithInvalidDataTest() {
// given: 생성자 데이터
List<String> invalidNameList = Arrays.asList(null, "", " ");

for (String invalidName : invalidNameList) {
// when
ThrowableAssert.ThrowingCallable constructorCall = () -> new Racer(invalidName);

// then
assertThatThrownBy(constructorCall)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(Racer.VALIDATE_NAME_ERROR_MESSAGE);
}
}
}
```

+ static 키워드에 대한 테스트에 대해서 강사님은 어떻게 생각하시는지?
+ entity, controller, view 여러 곳에 퍼져있는 validate 로직을 통합하는게 나은지, 아니면 지금대로 분산시켜서 각각 검증 로직을 들고 있는게 나은건지?
+ Test case, Test Fixture 등을 쉽게 만들어주는 라이브러리가 있는지? 아님 방법론이라던가?!
84 changes: 83 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,83 @@
# java-racingcar-precourse
# java-racingcar-precourse

# 기능 목록

## Model

### Racer Entity

#### Data
- [x] 자동차 경주자는 이름이 있다.
- [x] NAME_MAX_LENGTH 이하의 길이여야 한다.
- [x] 자동차 경주자는 이동한 거리를 정수로 기록한다.

#### Act
- [x] 자동차 경주자는 전진할 수 있다.
- [x] 전진할 때 숫자로 된 입력을 받는다.
- [x] 입력받은 숫자가 threshold 값을 넘어야 전진한다.
- [x] threshold 값을 넘지 못하면 전진하지 않는다.
- [ ] ~~자동차 경주자는 이름과 함께 이동한 거리를 출력할 수 있다.~~
- [x] 자동차 경주자는 숫자로 된 입력을 받아, 우승했는지 여부를 알려준다.

### Racer VO

#### Data
- [x] 자동차 경주자는 이름이 있다.
- [x] 자동차 경주자는 이동한 거리를 정수로 기록한다.
- [x] 자동차 경주자는 우승 여부를 기록한다.

### Controller

#### Data
- [x] 자동차 경주자 목록을 가지고 있는다.
- [x] 자동차 경주 진행 시도 횟수를 가지고 있는다.
- [x] 현재 자동차 경주 진행 횟수를 가지고 있는다.

#### Act
- [x] 자동차 이름을 1개의 문자열로 입력받아 자동차 목록을 생성한다.
- [x] 자동차 이름은 쉼표를 기준으로 구분하며, 이름이 NAME_MAX_LENGTH보다 길 경우 IllegalArgumentException을 발생시킨다.
- [x] 경주 게임을 진행할 횟수를 입력 받는다.
- [x] 횟수에는 최댓값 제한은 없으며, 음수 또는 숫자가 아닐 시 IllegalArgumentException을 발생시킨다.
- [x] 입력 받은 숫자로 경주 진행 시도 횟수를 초기화하고, 현재 자동차 경주 진행 횟수는 0으로 리셋한다.
- [x] 경주 게임을 1번 진행한다.
- [x] 만약 자동차 목록 또는 경주 게임 진행 횟수가 세팅이 안 되었을 경우 IllegalStateException을 발생시킨다.
- [x] 이미 게임이 종료 되었을 경우 IllegalStateException을 발생시킨다.
- [x] 게임이 끝나면 Racer VO 목록과 게임 종료 여부를 반환한다.

### View

#### Data
- [x] 자동차 경주 컨트롤러를 가지고 있는다.

#### Act
- [x] 자동차 경주 게임을 시작한다.
- [x] 경주할 자동차 이름을 입력받는다.
- [x] 시도할 횟수를 입력 받는다.
- [x] 자동차 경주 게임을 진행하며 매번 결과를 출력한다.
- [x] 최종 우승자를 출력한다.
- [x] 입력을 받을 때 에러가 발생하면 "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.

### Util

### RandomNumberGenerator

#### Act
- [x] 입력 받은 숫자 범위에서 무작위 정수를 생성한다.

### ~~ErrorMessageGenerator~~

#### ~~Act~~
- [ ] ~~입력 받은 에러 메세지를 정해진 포맷으로 바꿔서 반환한다.~~

# 후기

기능 단위를 먼저 쭉 정리하고 개발을 시작하니까 좀 더 수월하게 할 수 있었습니다. 고민도 미리 하고 어느정도 설계가 나온 상태여서 그런 것 같습니다. 물론 진행 과정에서 여러모로 수정이 있었지만, 크게 수정된 것은 없었던 것 같습니다.


RandomNumberGenerator를 Mocking하는 과정에서, mocking은 static 키워드와 바이트 코드 수준에서의 선언 순서로 인한 문제점이 있다는 것을 처음 알게 되었습니다. 여러 해결 방법을 찾아보고 직접 적용해볼 수 있어서 흥미로웠습니다. 그동안 별 생각 없이 static 키워드와 함께 Util 클래스를 자주 사용하고, 테스트에서도 mockStatic를 사용했습니다. 근데 테스팅 과정에서 static은 bad pattern 이라는 의견도 많이 보여서 스스로 static이 포함된 테스트 코드의 작성 난이도를 고민해보게 되었습니다.


Generator를 싱글톤 또는 static 메소드로 쓰고는 싶은데, static 키워드 때문에 테스팅이 어려워져서 고민이 좀 많아졌습니다. Spring을 쓰면 Util class를 Bean으로 등록해서 싱글톤으로 쓰면 되지만, 이번 프로그램에서는 DI Container 같은 것 없이 직접 의존성을 해결해줘야 하다 보니까 더 고민이 많았네요.


고민거리가 많았던 토이 프로젝트라 재밌었습니다ㅎㅎ
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ repositories {
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testImplementation 'org.assertj:assertj-core:3.25.3'
testImplementation 'org.mockito:mockito-core:5.12.0'
}

test {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/RacerApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import controller.RacerController;
import view.RacerView;

public class RacerApplication {
public static void main(String[] args) {
RacerView racerView = new RacerView(new RacerController());

while (true) {
racerView.render();
}
}
}
117 changes: 117 additions & 0 deletions src/main/java/controller/RacerController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package controller;

import dto.RacerDto;
import dto.RacerResult;
import entity.Racer;
import utils.RandomNumberGenerator;

import java.math.BigInteger;
import java.util.*;

public class RacerController {
public static final String VALIDATE_GAME_COUNT_ERROR_MESSAGE = "gameCount는 null이거나 음수일 수 없습니다.";

public static final String VALIDATE_RACER_LIST_ERROR_MESSAGE = "racerList는 null이거나 빈 배열일 수 없습니다.";

public static final String VALIDATE_GAME_ENDED_ERROR_MESSAGE = "이미 종료된 게임입니다.";

private List<Racer> racerList;

private BigInteger maxGameCount;

private BigInteger currentGameCount;

public RacerController() {
this.racerList = new ArrayList<>();
this.maxGameCount = new BigInteger("-1");
this.currentGameCount = new BigInteger("-1");
}

/**
* @throws IllegalArgumentException nameString이 null 또는 빈 문자열일 때, ","를 기준으로 split 했을 때 빈 문자열인 경우
*/
public void setUp(List<String> nameList, BigInteger input) {
validateGameCount(input);

List<Racer> newRacerList = nameList.stream()
.map(Racer::new)
.toList();

maxGameCount = input;
currentGameCount = BigInteger.ZERO;

racerList.clear();
racerList.addAll(newRacerList);
}

public RacerResult playGame() {
validatePlayGame();

currentGameCount = currentGameCount.add(BigInteger.ONE);

for (Racer racer : racerList) {
int randomInteger = RandomNumberGenerator.getInstance().getRandomNumber(0, 9);
racer.moveIfCan(randomInteger);
}

return new RacerResult(
isEnded(),
racerList.stream().map(this::getRacerDto).toList()
);
}

public boolean isEnded() {
return maxGameCount.equals(currentGameCount);
}

private RacerDto getRacerDto(Racer racer) {
if (isEnded()) {
return RacerDto.of(racer, getMax());
}

return new RacerDto(
racer.getName(),
racer.getMovedDistance(),
false
);
}

private BigInteger getMax() {
if (isEnded()) {
return racerList.stream()
.map(Racer::getMovedDistance)
.max(BigInteger::compareTo)
.orElseThrow(() -> new IllegalStateException(VALIDATE_RACER_LIST_ERROR_MESSAGE));
}

return null;
}

private void validateName(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(Racer.VALIDATE_NAME_ERROR_MESSAGE);
}
}

private void validateRacerList() {
if (racerList == null || racerList.isEmpty()) {
throw new IllegalStateException(VALIDATE_RACER_LIST_ERROR_MESSAGE);
}
}

private void validateGameCount(BigInteger integer) {
if (integer == null || integer.compareTo(BigInteger.ZERO) < 0) {
throw new IllegalArgumentException(VALIDATE_GAME_COUNT_ERROR_MESSAGE);
}
}

private void validatePlayGame() {
validateRacerList();
validateGameCount(maxGameCount);
validateGameCount(currentGameCount);

if (isEnded()) {
throw new IllegalStateException(VALIDATE_GAME_ENDED_ERROR_MESSAGE);
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/dto/RacerDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dto;

import entity.Racer;

import java.math.BigInteger;

public record RacerDto(
String name,

BigInteger movedDistance,

boolean isWinner
) {
public static RacerDto of(Racer racer, BigInteger input) {
return new RacerDto(
racer.getName(),
racer.getMovedDistance(),
racer.isWinner(input)
);
}
}
10 changes: 10 additions & 0 deletions src/main/java/dto/RacerResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dto;

import java.util.Collection;

public record RacerResult (
boolean isEnded,

Collection<RacerDto> racerDtos
) {
}
49 changes: 49 additions & 0 deletions src/main/java/entity/Racer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package entity;

import java.math.BigInteger;

public class Racer {
public static final int NAME_MAX_LENGTH = 5;

public static final int MOVE_THRESHOLD = 3;

public static final String VALIDATE_NAME_ERROR_MESSAGE = "name은 null이거나 빈 문자열일 수 없습니다. 그리고 " + NAME_MAX_LENGTH + "자 이하 여야합니다.";

private String name;

private BigInteger movedDistance;

/**
* @throws IllegalArgumentException name이 null이거나 blank 문자열 일 때
*/
public Racer(String name) {
this.name = validateName(name);
this.movedDistance = new BigInteger("0");
}

public void moveIfCan(int input) {
if(input > MOVE_THRESHOLD) {
movedDistance = movedDistance.add(new BigInteger("1"));
}
}

public boolean isWinner(BigInteger input) {
return input.compareTo(movedDistance) <= 0;
}

public String getName() {
return name;
}

public BigInteger getMovedDistance() {
return movedDistance;
}

private String validateName(String name) {
if (name == null || name.isBlank() || name.trim().length() > NAME_MAX_LENGTH) {
throw new IllegalArgumentException(VALIDATE_NAME_ERROR_MESSAGE);
}

return name.trim();
}
}
28 changes: 28 additions & 0 deletions src/main/java/utils/RandomNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package utils;

import java.util.Random;

public class RandomNumberGenerator {
private RandomNumberGenerator() {

}

public static RandomNumberGenerator getInstance() {
return LazyHolder.INSTANCE;
}

public int getRandomNumber(int start, int end) {
if (start > end) {
int temp = start;
start = end;
end = temp;
}

return LazyHolder.random.nextInt((end - start) + 1) + start;
}

private static class LazyHolder {
private static final Random random = new Random();
private static final RandomNumberGenerator INSTANCE = new RandomNumberGenerator();
}
}
Loading