> TDD(Test Driven Development) 개발 방식이란? <
- 테스트 코드를 먼저 작성하여 프로그램이 잘못됐다는 것을 증명하고, 이를 고쳐나가면 프로그램을 개발해 나가는 개발 방법론
=> 잘못된 점을 찾고 수정하는 과정을 계속 반복하며, 더 이상 잘못된 점을 찾을 수 없을 때 프로그램이 올바르다는 의미
🔆 자동차 경주
초간단 자동차 경주 게임을 구현한다.
✅ 기능 요구사항
- n
대의 자동차는 1칸 정지
or 정지
한다.
- 사용자로부터 자동차 이름
과 시도할 횟수
를 입력받는다.
- 자동차 이름은 전진하는 자동차를 출력할 때 사용한다.
- 자동차 이름은 쉼표 ,
를 기준으로 구분하며 이름은 5자 이하
만 가능하다.
- 전진하는 조건 : 0 ~ 9 사이 무작위 값 중 4 이상
인 경우
- 게임 완료 후 우승자를 출력한다. (한 명 이상 가능)
- 우승자가 여러 명이면 쉼표 ,
를 이용하여 구분한다.
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException
을 발생시킨다.
✅ 입출력 요구사항
- 입력 : 경주할 자동차 이름
(,
로 구분), 시도할 횟수
- 출력 : 차수별 실행 결과
, 단독 우승자 안내 문구
, 공동 우승자 안내 문구
- 실행 결과
경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)
pobi,woni,jun
시도할 횟수는 몇 회인가요?
5
실행 결과
pobi : -
woni :
jun : -
pobi : --
woni : -
jun : --
pobi : ---
woni : --
jun : ---
pobi : ----
woni : ---
jun : ----
pobi : -----
woni : ----
jun : -----
최종 우승자 : pobi, jun
TDD 과정
질문
1. 사용자에게 입력받은 자동차 이름 문자열에서 잘 추출하여 데이터로 보관할 수 있나?
> 한 대, 여러 대
2. 랜덤 값이 올바른 범위에서 생성되나?
3. 조건에 따른 이동 거리 계산이 올바른가?
> 전진 조건, 정지 조건
4. 자동차 위치가 올바르게 변하나?
5. 우승자를 올바르게 선정하나?
> 한 명, 여러 명
6. 사용자에게 입력받은 시도 횟수만큼 턴이 제대로 수행되나?
7. 초기 입력이 출력 형식을 준수하나?
8. 각 턴의 실행 결과가 출력 형식을 준수하나?
9. 우승자 결과가 출력 형식을 준수하나?
> 한 명, 여러 명
10. 자동차 이름이 유효하지 않으면 IllegalArgumentException
이 발생되나?
> 5글자 초과, 중복, 공백
11. 시도할 횟수가 유효하지 않으면 IllegalArgumentException
이 발생되나?
> 숫자 아님, 음수
응답
1. 사용자에게 입력받은 자동차 이름 문자열에서 잘 추출하여 데이터로 보관할 수 있나?
class RacingServiceTest {
private RacingService racingService;
@BeforeEach
void setUp() {
racingService = new RacingService("a,b,c", "4");
}
@Test
@DisplayName("입력받은 여러 자동차가 LinkedHashMap으로 올바르게 생성되는지 확인")
void 자동차_추출_테스트1() {
racingService.setCarNameInput("aa,bb,cc");
assertThat(racingService.getCarPositions()).hasSize(3);
assertThat(racingService.getCarPositions().keySet()).contains("cc");
}
@Test
@DisplayName("입력받은 한 자동차가 LinkedHashMap으로 올바르게 생성되는지 확인")
void 자동차_추출_테스트2() {
racingService.setCarNameInput("aa");
assertThat(racingService.getCarPositions()).hasSize(1);
assertThat(racingService.getCarPositions().keySet()).contains("aa");
}
}
RacingService 생성자
와 setCarNameInput
, getCarPositions
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingService
클래스에 테스트를 위해 임의로 carNameInput
을 설정하는 setCarNameInput
와 각각의 자동차 이름과 위치를 담는 carPositions
를 가져오는 getCarPositions
를 만들어주어 해당 테스트가 green
을 띄도록 해준다.
public class RacingService {
private final HashMap<String, Integer> carPositions = new HashMap<>();
private static final String CAR_INPUT_DELIMITER = ",";
private static final int INITIAL_POSITION = 0;
private final int maxTurn;
public RacingService(String carNameInput, String maxTurnInput) {
initializeCars(carNameInput);
this.maxTurn = Integer.parseInt(maxTurnInput);
}
private void initializeCars(String carNameInput) {
for (String carName : carNameInput.split(CAR_INPUT_DELIMITER)) {
carPositions.put(carName, INITIAL_POSITION);
}
}
public void setCarNameInput(String carNameInput) {
carPositions.clear();
initializeCars(carNameInput);
}
public HashMap<String, Integer> getCarPositions() {
return new HashMap<>(carPositions);
}
}
생성자를 통해 carNameInput
과 maxTurnInput
을 입력받는다.
carNameInput
을 split
해서 carName
과 carPosition
을 키와 값으로 LinkedHashMap
에 담아주는 initializeCars
메서드를 생성하여 생성자 안에서 호출한다.
[🔥짚고 넘어갈 부분!!]
- HashMap
을 선택한 이유
키와 값을 쌍으로 저장하기 위하여 HashMap
으로 정하였다. 근데 추후에 LinkedHashMap
으로 변경된다.
HashMap
은 저장을 하며 순서를 기억하지 않아 순서가 임의로 바뀔 수도 있다. 나중에 우승자 출력이 여러 명일 때, 입력받은 순서를 기억하여 그 순서대로 출력하기 위해 LinkedHashMap
로 변경하게 되었다.
- getCarPositions
에서 새로운 HashMap
반환하는 이유
carPositions
를 그대로 return하게 되면, 주소값이 전달되므로 불변성에 문제가 생길 수 있다. 따라서 새로운 객체를 반환하여 불변성을 유지하였다.
2. 랜덤 값이 올바른 범위에서 생성되나?
class RacingServiceTest {
...
@Test
@DisplayName("랜덤 값이 올바른 범위(0-9)에 있는지 확인")
void 랜덤_값_테스트() {
int result = racingService.createRandomValue();
assertThat(result).isBetween(0, 9);
}
}
createRandomValue
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingService
클래스에 랜덤 값을 생성하는 createRandomValue
를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingService {
...
private static final int RANDOM_MIN = 0;
private static final int RANDOM_MAX = 9;
public int createRandomValue() {
return Randoms.pickNumberInRange(RANDOM_MIN, RANDOM_MAX);
}
}
RANDOM_MIN
과 RANDOM_MAX
사이 랜덤 값을 생성해 주어 반환한다.
3. 조건에 따른 이동 거리 계산이 올바른가?
class RacingServiceTest {
...
@Test
@DisplayName("전진 조건에 따른 이동 거리 계산이 올바른지 확인")
void 이동_거리_테스트1() {
int result = racingService.calculateMoveDistance(5);
assertThat(result).isEqualTo(1);
}
@Test
@DisplayName("정지 조건에 따른 이동 거리 계산이 올바른지 확인")
void 이동_거리_테스트2() {
int result = racingService.calculateMoveDistance(1);
assertThat(result).isEqualTo(0);
}
}
calculateMoveDistance
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingService
클래스에 이동 거리를 계산하는 calculateMoveDistance
를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingService {
...
private static final int MOVE_THRESHOLD = 4;
private static final int MOVE_STEP = 1;
private static final int STOP_STEP = 0;
public int calculateMoveDistance(int randomValue) {
if (randomValue >= MOVE_THRESHOLD) {
return MOVE_STEP;
}
return STOP_STEP;
}
}
randomValue
를 받아 4
이상이면 1
(전진할 거리), 4
미만이면 0
(전진할 거리) 을 반환한다.
4. 자동차 위치가 올바르게 변하나?
class RacingServiceTest {
...
@Test
@DisplayName("자동차의 위치가 올바르게 변하는지 확인")
void 자동차_위치_테스트() {
int beforePosition = racingService.getCarPositions().get("a");
racingService.moveCar("a", 4);
assertThat(racingService.getCarPositions().get("a")).isGreaterThan(beforePosition);
}
}
moveCar
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingService
클래스에 carName
과 randomValue
를 받아 해당하는 거리만큼 이동하는 moveCar
를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingService {
...
public void moveCar(String carName, int randomValue) {
int moveDistance = calculateMoveDistance(randomValue);
carPositions.put(carName, carPositions.get(carName) + moveDistance);
}
}
calculateMoveDistance
를 통해 randomValue
로부터 이동할 거리를 받고, carName
에 이동할 거리를 더해준다.
[🔥짚고 넘어갈 부분!!]
- carPositions
에 put
해도 되나?
hashMap
은 중복된 키를 가질 수 없으므로 carName
과 carPosition
데이터를 넣어도 hashMap
에 추가되는 것이 아닌, 기존의 carName
값에서 새로운 값으로 업데이트된다.
5. 우승자를 올바르게 선정하나?
class RacingServiceTest {
...
@Test
@DisplayName("최대 이동 거리를 가진 한 명의 사람을 우승자로 올바르게 반환하는지 확인")
void 한_명의_우승자_테스트() {
racingService.moveCar("a", 4);
racingService.moveCar("b", 2);
racingService.moveCar("c", 1);
String[] result = racingService.getWinners();
assertThat(result).containsExactly("a");
}
@Test
@DisplayName("최대 이동 거리를 가진 여러 명의 사람을 우승자로 올바르게 반환하는지 확인")
void 여러_명의_우승자_테스트() {
racingService.moveCar("a", 4);
racingService.moveCar("b", 8);
racingService.moveCar("c", 1);
String[] result = racingService.getWinners();
assertThat(result).containsExactly("a", "b");
}
}
getWinners
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingService
클래스에 우승자를 반환하는 getWinners
를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingService {
...
public String[] getWinners() {
int maxPosition = getMaxPosition();
Set<String> winners = new HashSet<>();
for (Map.Entry<String, Integer> carPosition : carPositions.entrySet()) {
if (carPosition.getValue() == maxPosition) {
winners.add(carPosition.getKey());
}
}
return winners.toArray(new String[0]);
}
private int getMaxPosition() {
int maxPosition = INITIAL_POSITION;
for (Integer curPosition : carPositions.values()) {
if (curPosition > maxPosition) {
maxPosition = curPosition;
}
}
return maxPosition;
}
}
carPositions
를 통해 maxPosition
을 알려주는 getMaxPosition
메서드를 생성한다.
winners
를 담는 HashSet
을 생성하여 각 carPosition
이 maxPosition
인 carName
을 담아 배열로 변환하여 반환한다.
[🔥짚고 넘어갈 부분!!]
- HashSet
을 선택한 이유
키를 저장하고 탐색하기 쉽게 하기 위하여 HashSet
으로 정하였다. 근데 추후에 LinkedList
으로 변경된다.
HashSet
은 저장을 하며 순서를 기억하지 않아 순서가 임의로 바뀔 수도 있다. 나중에 우승자 출력이 여러 명일 때, 입력받은 순서를 기억하여 그 순서대로 출력하기 위해 LinkedList
로 변경하게 되었다.
6. 사용자에게 입력받은 시도 횟수만큼 턴이 제대로 수행되나?
class RacingServiceTest {
...
@Test
@DisplayName("입력받은 시도 횟수만큼 턴이 수행되는지 확인")
void 시도_횟수_확인_테스트() {
racingService.startRace();
int result = racingService.currentTurn;
assertThat(result).isEqualTo(4);
}
}
startRace
와 currentTurn
을 찾을 수 없어 테스트에 실패하게 된다.
=> RacingService
클래스에 랜덤 값을 생성하는 시도할 횟수만큼 턴이 실행되는 레이스를 시작하는 startRace
과 현재 턴 횟수를 담는 currentTurn
변수를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingService {
...
private final LinkedList<HashMap<String, Integer>> raceResult = new LinkedList<>();
public int currentTurn = 0;
public void startRace() {
while (currentTurn < maxTurn) {
raceResult.add(executeTurn());
currentTurn++;
}
}
private HashMap<String, Integer> executeTurn() {
for (String carName : carPositions.keySet()) {
int randomValue = createRandomValue();
moveCar(carName, randomValue);
}
return new HashMap<>(carPositions);
}
}
한 턴동안 모든 자동차의 위치가 업데이트된 carPosotions
를 반환하는 executeTurn
메서드를 생성한다.
startRace
는 LinkedList
로 만든 raceResult
에 각각의 실행 결과인 executeTurn
결과를 넣어준다.
7. 초기 입력이 출력 형식을 준수하나?
public class RacingIOTest {
private RacingService racingService;
private ByteArrayOutputStream outputStreamCaptor;
private PrintStream standardOut;
@BeforeEach
void setUp() {
racingService = new RacingService("a,b,c", "4");
standardOut = System.out;
outputStreamCaptor = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputStreamCaptor));
}
@AfterEach
void restoresStreams() {
System.setOut(standardOut);
System.out.println(getOutput());
}
protected String getOutput() {
return outputStreamCaptor.toString();
}
}
우선 터미널에 출력되는 결과를 확인하기 위해 위의 코드를 작성해 준다.
(대충 터미널에 입력되는 결과를 중간에 가로채 테스트 코드에서 확인하고, 다 실행한 후에 다시 원래대로 바꿔주는 내용이다.)
public class RacingIOTest {
...
@Test
@DisplayName("초기 입력 프롬프트가 출력 형식을 준수하는지 확인")
void 초기_입력_프롬프트_테스트() {
RacingIO.promptCarNameInput();
RacingIO.promptMaxTurnInput();
assertThat(getOutput()).contains("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
assertThat(getOutput()).contains("시도할 횟수는 몇 회인가요?");
}
}
RacingIO
클래스와 promptCarNameInput
, promptMaxTurnInput
을 찾을 수 없어 테스트에 실패하게 된다.
=> RacingIO
클래스에 carName
을 입력받고, 문구를 출력하는 promptCarNameInput
와 maxTurn
을 입력 받고, 문구를 출력하는promptMaxTurnInput
생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingIO {
private enum Messages {
CAR_NAME_INPUT_PROMPT("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"),
MAX_TURN_INPUT_PROMPT("시도할 횟수는 몇 회인가요?"),
private final String message;
Messages(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
public static void promptCarNameInput() {
System.out.println(Messages.CAR_NAME_INPUT_PROMPT.getMessage());
}
public static void promptMaxTurnInput() {
System.out.println(Messages.MAX_TURN_INPUT_PROMPT.getMessage());
}
}
초기 입력에 필요한 문구들을 enum
을 사용하여 상수로 만들고, 이를 각각 출력한다.
8. 각 턴의 실행 결과가 출력 형식을 준수하나?
public class RacingIOTest {
...
@Test
@DisplayName("각 턴의 실행 결과가 출력 형식을 준수하는지 확인")
void 실행_결과_출력_형식_테스트() {
LinkedList<HashMap<String, Integer>> raceResult = new LinkedList<>();
updateAndRecordMovement("a", 4, raceResult);
updateAndRecordMovement("a", 4, raceResult);
updateAndRecordMovement("b", 4, raceResult);
updateAndRecordMovement("c", 1, raceResult);
RacingIO.printRaceResult(raceResult);
assertThat(getOutput()).contains("실행 결과");
assertThat(getOutput()).contains("a : -");
assertThat(getOutput()).contains("a : --");
assertThat(getOutput()).contains("b : -");
assertThat(getOutput()).contains("c : ");
}
private void updateAndRecordMovement(String carName, int randomValue, LinkedList<HashMap<String, Integer>> raceResult) {
racingService.moveCar(carName, randomValue);
raceResult.add(new HashMap<>(racingService.getCarPositions()));
}
}
printRaceResult
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingIO
클래스에 raceResult
를 출력 형식에 맞게 출력하는 printRaceResult
를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingIO {
private enum Messages {
CAR_NAME_INPUT_PROMPT("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"),
MAX_TURN_INPUT_PROMPT("시도할 횟수는 몇 회인가요?"),
TURN_RESULT_PROMPT("실행 결과");
...
}
public static void printRaceResult(LinkedList<HashMap<String, Integer>> raceResult) {
System.out.println();
System.out.println(Messages.TURN_RESULT_PROMPT.getMessage());
for (HashMap<String, Integer> turnResult : raceResult) {
printTurnResult(turnResult);
System.out.println();
}
}
private static void printTurnResult(HashMap<String, Integer> turnResult) {
turnResult.forEach((carName, carPosition) ->
System.out.println(carName + " : " + "-".repeat(carPosition))
);
System.out.println();
}
}
enum
에 TURN_RESULT_PROMPT
를 추가해 준다.
각 턴을 담는 raceResult
를 순회하며, 턴 결과를 이름 : --
형식으로 출력해 주는 printTurnResult
를 호출하는 printRaceResult
를 생성한다.
9. 우승자 결과가 출력 형식을 준수하나?
public class RacingIOTest {
...
@Test
@DisplayName("우승자 결과가 출력 형식을 준수하는지 확인")
void 우승자_출력_형식_테스트() {
LinkedList<HashMap<String, Integer>> raceResult = new LinkedList<>();
updateAndRecordMovement("a", 4, raceResult);
updateAndRecordMovement("b", 4, raceResult);
RacingIO.promptWinner(racingService.getWinners());
assertThat(getOutput()).contains("최종 우승자 : a, b");
}
@Test
@DisplayName("우승자가 여러 명일 때 우승자 입력 순서대로 출력하는지 확인")
void 우승자_출력_순서_테스트() {
racingService.setCarNameInput("a,aa,aaa");
LinkedList<HashMap<String, Integer>> raceResult = new LinkedList<>();
updateAndRecordMovement("a", 4, raceResult);
updateAndRecordMovement("aa", 4, raceResult);
updateAndRecordMovement("aaa", 4, raceResult);
RacingIO.promptWinner(racingService.getWinners());
assertThat(getOutput()).contains("최종 우승자 : a, aa, aaa");
}
}
promptWinner
를 찾을 수 없어 테스트에 실패하게 된다.
=> RacingIO
클래스에 우승자를 출력 형식대로 출력하는 promptWinner
를 생성하여 해당 테스트가 green
을 띄도록 해준다.
public class RacingIO {
private static final String WINNER_DELIMITER = ", ";
private enum Messages {
CAR_NAME_INPUT_PROMPT("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"),
MAX_TURN_INPUT_PROMPT("시도할 횟수는 몇 회인가요?"),
TURN_RESULT_PROMPT("실행 결과"),
WINNER_PROMPT("최종 우승자 : ");
...
}
public static void promptWinner(String[] winners) {
String joinedWinners = String.join(WINNER_DELIMITER, winners);
System.out.println(Messages.WINNER_PROMPT.getMessage() + joinedWinners);
}
}
enum
에 WINNER_PROMPT
를 추가해 준다.
우승자를 담는 배열 winners
에 WINNER_DELEMITER
로 join
하여 출력해 준다.
[🔥짚고 넘어갈 부분!!]
- HashMap
과 HashSet
으로 저장하면 안 됐다!!!
만약에 자동차 이름이 a
, aa
, aaa
이고 모두 우승자라면 최종 우승자 : a, aa, aaa
로 출력되지 않고 최종 우승자 : aa, aaa, a
로 출력되는 것을 확인했다. 찾아보니 HashMap
과 HashSet
은 저장을 하며 순서를 기억하지 않아 순서가 임의로 바뀌어 저장될 수도 있었다는 것을 알게 되었다. 예시를 보면 공동 우승자가 발생하면 입력한 순서대로 출력을 하기 때문에, 입력받은 순서를 기억하여 그 순서대로 출력해야 해서 LinkedHashMap
과 LinkedList
로 데이터 저장 방식을 바꿔주어 순서를 기억하도록 하였다.
10. 자동차 이름이 유효하지 않으면 IllegalArgumentException
이 발생되나?
public class ValidatorTest {
private RacingService racingService;
@Test
@DisplayName("자동차 이름이 5글자 초과인 경우")
void 자동차_이름_유효성_테스트1() {
assertThatThrownBy(() -> {
racingService = new RacingService("aaaaaaa,bbbbbbb,ccccccc,ddddddd", "4");
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName(" 자동차 이름이 중복된 경우")
void 자동차_이름_유효성_테스트2() {
assertThatThrownBy(() -> {
racingService = new RacingService("a,a,b,c,d", "4");
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("자동차 이름이 공백인 경우")
void 자동차_이름_유효성_테스트3() {
assertThatThrownBy(() -> {
racingService = new RacingService("a,,,b,,,", "4");
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
}
발생하는 예외가 IllegalArgumentException
가 아니어서 테스트에 실패하게 된다.
=> Validator
클래스에 자동차 이름을 검사하는 validateCarName
를 생성하여 IllegalArgumentException
을 발생시켜 해당 테스트가 green
을 띄도록 해준다.
public class Validator {
private static final int MAX_LENGTH = 5;
private enum ErrorMessage {
MUST_BE_5_CHARACTERS_OR_LESS("자동차 이름이 5글자 이하여야 합니다."),
MUST_BE_UNIQUE("자동차 이름이 중복되지 않아야 합니다."),
MUST_BE_FILLED("자동차 이름이 공백이지 않아야 합니다.");
private final String message;
ErrorMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
public static void validateCarName(LinkedHashMap<String, Integer> carPositions, String carName) {
validateNotEmpty(carName);
validateLength(carName);
validateUnique(carPositions, carName);
}
public static void validateMaxTurn(String maxTurn) {
validateIsNumber(maxTurn);
}
private static void validateUnique(LinkedHashMap<String, Integer> carPositions, String carName) {
if (carPositions.containsKey(carName)) {
throw new IllegalArgumentException(ErrorMessage.MUST_BE_UNIQUE.getMessage());
}
}
private static void validateLength(String carName) {
if (carName.length() > MAX_LENGTH) {
throw new IllegalArgumentException(ErrorMessage.MUST_BE_5_CHARACTERS_OR_LESS.getMessage());
}
}
}
발생하는 에러 메시지를 enum
으로 만들어 주고, 각각 조건을 설정하여 유효하지 않으면 IllegalArgumentException
을 설정한 에러 메시지와 함께 출력하도록 하였다.
public class RacingService {
...
private void initializeCars(String carNameInput) {
for (String carName : carNameInput.split(CAR_INPUT_DELIMITER, -1)) {
Validator.validateCarName(carPositions, carName);
carPositions.put(carName, INITIAL_POSITION);
}
}
}
그리고 RacingService
클래스에 초기화하는 함수에서 carName
을 validateCarName
를 통해 유효성 검사를 하도록 추가해 주었다.
또한 자동차 이름 입력값을 split
할 때 빈 문자열이면 배열에 포함되지 않아 split
의 두 번째 인자에 -1
을 넣어 빈 문자열도 배열에 담기도록 하였다.
11. 시도할 횟수가 유효하지 않으면 IllegalArgumentException
이 발생되나?
public class ValidatorTest {
...
@Test
@DisplayName("시도할 횟수가 숫자가 아닌 경우")
void 시도할_횟수_유효성_테스트1() {
assertThatThrownBy(() -> {
racingService = new RacingService("a,b,c", "a");
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("시도할 횟수가 음수인 경우")
void 시도할_횟수_유효성_테스트1() {
assertThatThrownBy(() -> {
racingService = new RacingService("a,b,c", "-1");
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
}
발생하는 예외가 IllegalArgumentException
가 아니어서 테스트에 실패하게 된다.
=> Validator
클래스에 시도할 횟수를 검사하는 validateMaxTurn
를 생성하여 IllegalArgumentException
을 발생시켜 해당 테스트가 green
을 띄도록 해준다.
public class Validator {
private static final int MAX_LENGTH = 5;
private enum ErrorMessage {
MUST_BE_5_CHARACTERS_OR_LESS("자동차 이름이 5글자 이하여야 합니다."),
MUST_BE_UNIQUE("자동차 이름이 중복되지 않아야 합니다."),
MUST_BE_FILLED("자동차 이름이 공백이지 않아야 합니다."),
MUST_BE_NUMBER("시도할 횟수가 숫자여야 합니다."),
MUST_BE_ZERO_OR_MORE("시도할 횟수가 0 이상이어야 합니다.");
...
}
public static void validateMaxTurn(String maxTurn) {
validateIsNumber(maxTurn);
}
private static void validateIsNumber(String maxTurn) {
try {
int maxTurnToInt = Integer.parseInt(maxTurn);
validateNotNegative(maxTurnToInt);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(ErrorMessage.MUST_BE_NUMBER.getMessage());
}
}
private static void validateNotNegative(int maxTurn) {
if (maxTurn < 0) {
throw new IllegalArgumentException(ErrorMessage.MUST_BE_ZERO_OR_MORE.getMessage());
}
}
}
발생하는 에러 메시지를 enum
으로 만들어 주고, 각각 조건을 설정하여 유효하지 않으면 IllegalArgumentException
을 설정한 에러 메시지와 함께 출력하도록 하였다.
[🔥짚고 넘어갈 부분!!]
- try 문 안에서 error를 NumberFormatException
이라고 한 이유?
validateIsNumber
에서 catch 할 에러에 Exception e
라고 적고, validateNotNegative
에서 에러가 발생한다고 하자. 그러면 catch가 모든 error를 잡아내므로 validateNotNegative
에서 발생한 에러를 catch 하여 MUST_BE_ZERO_OR_MORE
메시지가 아닌 MUST_BE_NUMBER
메시지로 전달될 것이다. 따라서 NumberFormatException
이라고 명시해 주면 Integer.parseInt(maxTurn)
으로 발생한 에러만 잡고, validateNotNegative
에서 던지는 IllegalArgumentException
은 잡지 못하게 된다.
public class RacingService {
...
public RacingService(String carNameInput, String maxTurnInput) {
initializeCars(carNameInput);
Validator.validateMaxTurn(maxTurnInput);
this.maxTurn = Integer.parseInt(maxTurnInput);
}
...
}
그리고 RacingService
클래스의 생성자 함수에서 maxTurn
을 validateMaxTurn
를 통해 유효성 검사를 하도록 추가해 주었다.
정제
public class ValidatorTest {
...
@ParameterizedTest(name = "{1}")
@CsvSource({
"'aaaaaaa,bbbbbbb,ccccccc,ddddddd', 자동차 이름이 5글자 초과인 경우",
"'a,a,b,c,d', 자동차 이름이 중복된 경우",
"'a,,,b,,,', 자동차 이름이 공백인 경우"
})
void 자동차_이름_유효성_테스트(String carNameInput, String description) {
assertThatThrownBy(() -> {
racingService = new RacingService(carNameInput, "4");
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
@ParameterizedTest(name = "{1}")
@CsvSource({
"'a', 시도할 횟수가 숫자가 아닌 경우",
"'-1', 시도할 횟수가 음수인 경우"
})
void 시도할_횟수_유효성_테스트(String maxTurnInput, String description) {
assertThatThrownBy(() -> {
racingService = new RacingService("a,b,c", maxTurnInput);
racingService.startRace();
}).isInstanceOf(IllegalArgumentException.class);
}
}
class RacingServiceTest {
...
@ParameterizedTest(name = "{3}")
@CsvSource({
"'aa,bb,cc', 3, 'cc', 입력받은 여러 자동차를 LinkedHashMap 으로 생성",
"'aa', 1, 'aa', 입력받은 한 대의 자동차를 LinkedHashMap 으로 생성"
})
@DisplayName("자동차 이름 입력에 따라 LinkedHashMap이 올바르게 생성되는지 확인")
void 자동차_추출_테스트(String carNameInput, int size, String carName, String description) {
racingService.setCarNameInput(carNameInput);
assertThat(racingService.getCarPositions()).hasSize(size);
assertThat(racingService.getCarPositions().keySet()).contains(carName);
}
...
@ParameterizedTest(name = "{2}")
@CsvSource({
"5, 1, 전진 조건에 따른 이동해야 할 거리 확인",
"1, 0, 정지 조건에 따른 이동해야 할 거리 확인"
})
@DisplayName("전진 및 정지 조건에 따른 이동 거리 계산이 올바른지 확인")
void 이동_거리_테스트(int randomValue, int expected, String description) {
int result = racingService.calculateMoveDistance(randomValue);
assertThat(result).isEqualTo(expected);
}
...
}
JUnit5
의 ParameterizedTest
를 사용하여 중복된 패턴을 제거하고 유사한 테스트를 그룹화하였다.
public class Application {
public static void main(String[] args) {
new Application().runGame();
}
private void runGame() {
String carNameInput = getCarName();
String maxTurnInput = getMaxTurn();
RacingService racingService = new RacingService(carNameInput, maxTurnInput);
racingService.startRace();
RacingIO.printRaceResult(racingService.getRaceResult());
RacingIO.promptWinner(racingService.getWinners());
}
private String getCarName() {
RacingIO.promptCarNameInput();
return RacingIO.getInput();
}
private String getMaxTurn() {
RacingIO.promptMaxTurnInput();
return RacingIO.getInput();
}
}
public class RacingIO {
...
public static String getInput() {
return readLine();
}
...
}
RacingIO
에 입력 받는 getInput
을 추가하고, 이를 이용하여 입출력을 받고 RacingService
클래스의 비즈니스 로직을 실행하며 최종 결과를 RacingIO
를 통해 결과를 출력해주는 Application
클래스도 구현해주었다.

모든 테스트 케이스를 통과하였다 ~~~~~~~~~ !!
'💠프로젝트 및 경험 > 우테코 7기' 카테고리의 다른 글
[우테코 7기] 백엔드 프리코스 3주 차 회고 (0) | 2024.11.05 |
---|---|
[MVC 패턴] 로또 MVC 패턴을 이용하여 구현하기! (0) | 2024.11.04 |
[우테코 7기] 백엔드 프리코스 2주 차 회고 (1) | 2024.10.29 |
[TDD] 문자열 덧셈 계산기 TDD 방식으로 구현하기! (1) | 2024.10.21 |
[우테코 7기] 백엔드 프리코스 1주 차 회고 (0) | 2024.10.21 |