728x90
숫자야구게임 다시 구현
다시 재정비하는 마음으로 TDD에 대해서 정리부터 시작
1. TDD를 하는 이유
1. 디버깅 시간을 줄여준다
2. 동작하는 문서 역할을 한다
3. 변화에 대한 두려움을 줄여준다
2. TDD 사이클
1. 실패하는 테스트를 구현한다
2. 테스트가 성공하도록 프로덕션 코드를 구현한다
3. 프로덕션 코드와 테스트 코드를 리팩토링 한다
3. TDD 원칙
1. 실패하는 단위 테스트를 작성할 때 까지 프로덕션 코드를 작성하지 않는다
2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다
3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다
다시 초기화하고 새로 코드를 짰음
1. Ball 객체 - Ball.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Ball {
static final int BALLS_COUNT = 3;
private final int round;
private final int ball;
public Ball(int round, int ball) {
this.round = round;
this.ball = ball;
}
public static List<Ball> inputStringToBalls(String inputString) {
List<Integer> inputNumberList = Arrays.stream(inputString.split("")).map(Integer::parseInt).collect(Collectors.toList());
/* 유효성 체크 */
ballsValidateCheck(inputNumberList);
return inputListToBalls(inputNumberList);
}
private static void ballsValidateCheck(List<Integer> inputNumberList) {
/* 숫자가 3개가 아니거나 중복된 숫자가 있을 경우, 숫자 0이 있을 경우 */
if(inputNumberList.stream().distinct().count() != BALLS_COUNT || inputNumberList.contains(0)){
throw new IllegalArgumentException();
}
}
public static List<Ball> inputListToBalls(List<Integer> inputNumberList) {
List<Ball> ballList = new ArrayList<>();
for (int i = 0; i < inputNumberList.size(); i++) {
ballList.add(new Ball(i+1, inputNumberList.get(i)));
}
return ballList;
}
public int getRound() {
return round;
}
public int getBall() {
return ball;
}
public enum BallStatus {BALL, NOTHING, STRIKE}
}
1.1 TDD - BallTest.java
import baseball.model.Ball;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.internal.bytebuddy.matcher.ElementMatchers.is;
public class BallTest {
@DisplayName("입력받은 문자열을 List 형태로 타입 변경")
@Test
void inputStringToList(){
assertThat(Ball.inputStringToBalls("123"))
.usingRecursiveComparison()
.isEqualTo(Arrays.asList(new Ball(1,1),new Ball(2,2),new Ball(3,3)));
}
}
2. Ball 결과를 담을 객체 - BallsStatusResult.java
public class BallsStatusResult {
private final int strikeCount;
private final int ballCount;
public BallsStatusResult(int strikeCount, int ballCount) {
this.strikeCount = strikeCount;
this.ballCount = ballCount;
}
public int getStrikeCount() {
return strikeCount;
}
public int getBallCount() {
return ballCount;
}
}
3. 사용자 Ball과 정답(컴퓨터) Ball을 비교하는 로직 - BallCompare.java
import baseball.model.Ball;
import baseball.model.BallsStatusResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BallCompare {
public static Ball.BallStatus compareBall(Ball answerBall, Ball userBall) {
if(answerBall.getRound() == userBall.getRound() && answerBall.getBall() == userBall.getBall()){
return Ball.BallStatus.STRIKE;
}
if(answerBall.getBall() == userBall.getBall()){
return Ball.BallStatus.BALL;
}
return Ball.BallStatus.NOTHING;
}
public BallsStatusResult ballsCompareResult(List<Ball> answerBallList, List<Ball> userBallList) {
List<Ball.BallStatus> ballStatusList = new ArrayList<>();
for (Ball userBall : userBallList) {
ballStatusList.add(compareBalls(userBall, answerBallList));
}
int strike = Collections.frequency(ballStatusList, Ball.BallStatus.STRIKE);
int ball = Collections.frequency(ballStatusList, Ball.BallStatus.BALL);
return new BallsStatusResult(strike,ball);
}
public static Ball.BallStatus compareBalls(Ball userBall, List<Ball> answerBallList) {
Ball.BallStatus returnBallStatus = Ball.BallStatus.NOTHING;
int index = 0;
/* Strike 또는 Ball 이면 반복문 중단하고 return */
while (returnBallStatus.equals(Ball.BallStatus.NOTHING) && index < 3){
returnBallStatus = compareBall(userBall, answerBallList.get(index));
index++;
}
return returnBallStatus;
}
}
3.1 TDD - BallCompareTest.java
import baseball.model.Ball;
import baseball.model.BallsStatusResult;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class BallCompareTest {
@DisplayName("정답과 사용자가 입력한 숫자 비교")
@Test
void ballsCompareResult(){
BallCompare ballCompare = new BallCompare();
List<Ball> answerBallList = Arrays.asList(new Ball(1,1),new Ball(2,2),new Ball(3,3));
List<Ball> userBallList = Arrays.asList(new Ball(1,2),new Ball(2,1),new Ball(3,3));
BallsStatusResult ballsStatusResult = ballCompare.ballsCompareResult(answerBallList, userBallList);
BallsStatusResult answerBallsStatusResult = new BallsStatusResult(1,2);
assertThat(ballsStatusResult)
.usingRecursiveComparison()
.isEqualTo(answerBallsStatusResult);
}
@DisplayName("정답을 기준으로 사용자가 입력한 숫자 1개 비교")
@Test
void compareBalls(){
Ball userBall = new Ball(2,1);
List<Ball> answerBallList = Arrays.asList(new Ball(1,1),new Ball(2,2),new Ball(3,3));
assertThat(BallCompare.compareBalls(userBall, answerBallList)).isEqualTo(Ball.BallStatus.BALL);
}
@DisplayName("결과 값 1:1 비교 - nothing")
@Test
void compareBall_nothing(){
Ball answerBall = new Ball(1,3);
Ball userBall = new Ball(2,5);
assertThat(BallCompare.compareBall(answerBall, userBall)).isEqualTo(Ball.BallStatus.NOTHING);
}
@DisplayName("결과 값 1:1 비교 - ball")
@Test
void compareBall_ball(){
Ball answerBall = new Ball(1,3);
Ball userBall = new Ball(2,3);
assertThat(BallCompare.compareBall(answerBall, userBall)).isEqualTo(Ball.BallStatus.BALL);
}
@DisplayName("결과 값 1:1 비교 - strike")
@Test
void compareBall_strike(){
Ball answerBall = new Ball(1,3);
Ball userBall = new Ball(1,3);
assertThat(BallCompare.compareBall(answerBall, userBall)).isEqualTo(Ball.BallStatus.STRIKE);
}
}
4. 랜덤 수 생성 - CreateAnswerNumber.java
import java.util.ArrayList;
import java.util.List;
public class CreateAnswerNumber {
public static final int MIN_NUMBER = 1;
public static final int MAX_NUMBER = 9;
public List<Integer> CreateRandomNumberList(){
List<Integer> randomNumberList = new ArrayList<>();
while (randomNumberList.size() < 3){
randomNumberList = RandomNumberList(randomNumberList);
}
return randomNumberList;
}
public List<Integer> RandomNumberList(List<Integer> randomNumberList){
int randomNumber = RandomNumber();
if(!randomNumberList.contains(randomNumber)){
randomNumberList.add(randomNumber);
}
return randomNumberList;
}
public int RandomNumber(){
return (int)(Math.random() * (MAX_NUMBER - MIN_NUMBER)) + MIN_NUMBER;
}
}
5. 입력 UI - InputView.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class InputView {
/* 숫자 값 입력 */
public static String inputNumber() throws IOException {
System.out.print("숫자를 입력해 주세요 : ");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
return br.readLine();
}
/* 게임 시작 종료 선택 */
public int InputGameStartOrEnd() throws IOException {
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
return Integer.parseInt(br.readLine());
}
}
6. 출력 UI - ResultView.java
public class ResultView {
public static void GameResultMessage(int ball, int strike){
/* 결과 조합 */
StringBuilder resultStringBuilder = new StringBuilder();
if(strike == 3){
resultStringBuilder.append("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
}
if(ball == 0 && strike == 0){
resultStringBuilder.append("낫싱");
}
if(ball > 0){
resultStringBuilder.append(ball);
resultStringBuilder.append("볼 ");
}
if(strike > 0 && strike < 3){
resultStringBuilder.append(strike);
resultStringBuilder.append("스트라이크");
}
System.out.println(resultStringBuilder.toString());
}
}
7. Game 반복 실행 로직 - BaseballGame.java
import baseball.model.Ball;
import baseball.model.BallsStatusResult;
import java.io.IOException;
import java.util.List;
public class BaseballGame {
final int GAME_FINISH_STRIKE_COUNT = 3;
public void GamePlaying() throws IOException {
/* 컴퓨터(정답) 랜덤한 수 생성 */
CreateAnswerNumber createAnswerNumber = new CreateAnswerNumber();
List<Ball> answerBallList = Ball.inputListToBalls(createAnswerNumber.CreateRandomNumberList());
boolean clearFlag = false;
while (!clearFlag){
try{
/* 사용자 랜덤한 수 입력 */
List<Ball> userBallList = Ball.inputStringToBalls(InputView.inputNumber());
clearFlag = GameResultCheckAndReturnClearFlag(answerBallList, userBallList);
}catch (IllegalArgumentException e){
System.out.println("입력한 숫자가 올바르지 않습니다. 1부터 9까지 서로 다른 수로 이루어진 3자리의 수를 입력해주세요");
}
}
}
public int isGameReplayChoice() throws IOException {
InputView inputView = new InputView();
return inputView.InputGameStartOrEnd();
}
/* 게임 결과 확인 (숫자 비교, 결과 값 도출) */
public boolean GameResultCheckAndReturnClearFlag(List<Ball> answerBallList, List<Ball> userBallList){
BallCompare ballCompare = new BallCompare();
BallsStatusResult ballsStatusResult = ballCompare.ballsCompareResult(answerBallList, userBallList);
int ballCount = ballsStatusResult.getBallCount();
int strikeCount = ballsStatusResult.getStrikeCount();
ResultView.GameResultMessage(ballCount, strikeCount);
if(strikeCount == GAME_FINISH_STRIKE_COUNT){
return true;
}
return false;
}
}
8. main - Application.java
import java.io.IOException;
public class Application {
public static void main(String[] args) throws IOException {
BaseballGame baseballGame = new BaseballGame();
int inputGameStartOrEnd = 1;
while(inputGameStartOrEnd == 1){
baseballGame.GamePlaying();
inputGameStartOrEnd = baseballGame.isGameReplayChoice();
}
}
}
마무리 느낀점
굉장히 오래걸렸다...
평소 코딩하던 스타일을 모두 버리고 TDD 위주로 개발하는데 집중했다
그렇게 신경쓰면서 했는데도 불구하고 위에 적은 TDD 원칙을 못지킨 경우도 많았던 것 같다
하지만 확실히 중간중간 잘못된 코드를 수정해야할 일이 있을 때 TDD로 테스트 케이스를 만들어 놓은 경우
수정에 대한 부담감이 줄어드는걸 경험할 수 있었다
너무 어려웠다
모든 부분을 TDD로 테스트를 할 수는 없었고 랜덤한 수를 생성하는 로직의 경우는 어떤식으로 테스트해야할지 아직도 감이 안온다
많은 연습이 필요할 것 같고 남은 과제들을 풀어나가면서 학습해야겠다
나름대로 TDD, BDD를 해본다고 공부했던적이 있는데 독학은 역시...
확실히 개발은 피드백이 필요한 것 같다
728x90
'개발 교육 > 플레이그라운드 with TDD, 클린코드' 카테고리의 다른 글
자동차 경주 게임 구현 (0) | 2022.03.12 |
---|---|
TDD/리팩토링 실습 - 문자열 덧셈 계산기 (0) | 2022.03.09 |
숫자야구게임 구현 - 피드백 적용 전 (0) | 2022.03.06 |
단위 테스트 실습 - 문자열 계산기 (0) | 2022.03.02 |
AssertJ 맛보기(2) (0) | 2022.03.01 |