728x90

 


 

단위 테스트 실습 - 숫자야구게임

  • 다음 요구사항을 JUnit을 활용해 단위 테스트 코드를 추가해 구현한다.

 

 

 

기능 요구사항

  • 같은 수가 같은 자리에 있으면 스트라이크, 다른 자리에 있으면 볼, 같은 수가 전혀 없으면 포볼 또는 낫싱이란 힌트를 얻고, 그 힌트를 이용해서 먼저 상대방(컴퓨터)의 수를 맞추면 승리한다.
    • e.g. 상대방(컴퓨터)의 수가 425일 때, 123을 제시한 경우 : 1 스트라이크, 456을 제시한 경우 : 1 볼 1스트라이크, 789를 제시한 경우 : 낫싱
  • 위 숫자 야구 게임에서 상대방의 역할을 컴퓨터가 한다. 컴퓨터는 1에서 9까지 서로 다른 임의의 수 3개를 선택한다. 게 임 플레이어는 컴퓨터가 생각하고 있는 3개의 숫자를 입력하고, 컴퓨터는 입력한 숫자에 대한 결과를 출력한다.
  • 이 같은 과정을 반복해 컴퓨터가 선택한 3개의 숫자를 모두 맞히면 게임이 종료된다.
  • 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다.

 

 

 

실행 결과

숫자를 입력해 주세요 :

123

1볼 1스트라이크

숫자를 입력해 주세요 :

145

1볼

숫자를 입력해 주세요 :

671

2 볼

숫자를 입력해 주세요 :

216

1 스트라이크

숫자를 입력해 주세요 :

713

3 스트라이크

3개의 숫자를 모두 맞히셨습니다! 게임 종료 게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.

1

숫자를 입력해 주세요 :

123

1 볼 1 스트라이크

...

 

 

 


 

 

 

📖 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가

 

 

 

 

 

📖 기능 구현 코드

1부터 9까지의 서로 다른 임의의 수 3개를 생성하기. (GenerateRandomNum 클래스)

package baseball.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class GenerateRandomNum {
    public int randomMake(){
        Random random = new Random();
        return random.nextInt(9) + 1;
    }

    public List<Integer> create(){
        List<Integer> computerNumber = new ArrayList<>();
        while (computerNumber.size() < 3){
            int randomNumber = randomMake();
            if(computerNumber.contains(randomNumber)){
                continue;
            }else {
                computerNumber.add(randomNumber);
            }
        }
        return computerNumber;
    }
}

여기서는 주의해야 할 점이 있다. 바로 숫자는 1~9 중 하나만 나와야 한다는 점과 세 자리 숫자는 중복돼서는 안 된다는 점이다.

 

1) 우선 1~9 사이의 랜덤 한 값을 가져오기 위해 

random.nextInt(max) + min;

를 사용하면 된다. max에는 랜덤 값 중 가장 큰 값, min에는 랜덤값 중 가장 작은 값을 넣는다.

 

따라서 우리의 식에는 랜덤 값 중 최댓값은 9이고 최솟값은 1이므로 

random.nextInt(9) + 1;

이 들어가면 된다.

 

2) 또한 세 자리 숫자에서 중복을 막기 위해

while문에 조건문을 붙여 배열의 사이즈가 3이 될 때까지 만약 넣으려고 하는 숫자가 배열에 있다면 continue를 통해 넘어가 주고 없다면 배열에 추가해주는 방법을 사용해준다.

 

나는 여기서 랜덤 값을 생성하는 부분을 새로운 메서드로 분리해 조금 더 로직 구현이 명확하게 보이도록 리팩터링 과정을 거쳤다.

 

 

 

 

컴퓨터의 수(3자리)와 플레이어의 수(3자리)를 비교하기. (Compare 클래스)

package baseball.domain;

import java.util.List;

public class Compare {
    public int howMany(List<Integer> computer, List<Integer> player){
        int result = 0;
        for(int i = 0; i < player.size(); i++){
            if(computer.contains(player.get(i))){
                result += 1;
            }
        }
        return result;
    }

    public int countStrike(List<Integer> computer, List<Integer> player){
        int strike = 0;
        for(int i = 0; i < player.size(); i++){
            if(computer.get(i) == player.get(i)){
                strike += 1;
            }
        }
        return strike;
    }
}

howmany메서드

해당 메서드에서는 컴퓨터의 숫자와 플레이어의 숫자를 비교하여 얼마나 많은 숫자들이 동일한지(위치와는 상관없이)를 판단한다.

여기서 result에 반환되는 숫자는 스트라이크와 볼의 합이 된다.

예를 들면 result에 2가 반환됐다고 하면 2 스트라이크, 1 볼 1 스트라이크, 2 볼 중 하나가 나올 수 있는 것이다.

 

 

countStrike메서드

해당 메서드에서는 스트라이크수를 판단한다. 왜 볼의 수를 판단하는 메서드는 없냐고 묻는다면 위에 hommany에서 return 된 숫자가

스트라이크 수 + 볼 수 이므로 스트라이크 수만 결정된다면 볼 수도 자연스럽게 결정되기 때문이다.

매개변수로 컴퓨터 숫자 배열과 플레이어 숫자 배열을 받고 각각의 자리를 비교하여 자리와 숫자가 같다면 스트라이크 수를 하나씩 늘려주면서 스트라이크수를 결정한다.

 

 

 

 

 

스트라이크 볼 최종결정하기. (Judgement 클래스)

package baseball.domain;

import java.util.List;

public class Judge {
    Compare compare = new Compare();
    public String judgement(List<Integer> computer, List<Integer> player){
        int total = compare.howMany(computer, player);
        int strike = compare.countStrike(computer, player);
        int ball = total - strike;

        if(total == 0){
            return "낫싱";
        }else if(strike == 0){
            return ball + "볼";
        }else if(ball == 0){
            return strike + "스트라이크";
        }
        return ball + "볼 " + strike + "스트라이크";
    }
}

앞서 Compare클래스에서 판단한

  • total(위치 상관없이 몇 개의 숫자가 같은지)
  • strike(스트라이크 수)
  • ball(total에서 strike를 뺀 볼 수)

를 각각 경우를 조건문으로 해서 String 타입으로 반환해준다.

 

 

 

 

 

입력값 받기. (Input 클래스)

package baseball.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Input {
    public List<Integer> playerNumber(){
        System.out.println("숫자를 입력해주세요");
        Scanner scanner = new Scanner(System.in);
        List<Integer> playerNum = new ArrayList<>();
        String input = scanner.next();

        for(String number: input.split("")){
            playerNum.add(Integer.parseInt(number));
        }
        return playerNum;
    }
}

자바의 Scanner 클래스를 이용해서 플레이어의 입력값을 받는다.

 

여기서 주의해야 할 점은 scanner.next()로 받은 입력값은 String타입이므로

split을 통해 각 숫자(타입은 String)를 3개로 분리해주고 이를 parseInt로 int타입으로 변환해 주어야 한다는 점이다.

그런 다음 List <Integer> 타입으로 반환해준다.

 

 

 

 

 

경기 재시작 여부 확인 (Playagain 클래스)

package baseball.domain;

import java.util.Scanner;

public class Playagain {
    public boolean playagain(){
        System.out.println("축하합니다! 경기를 다시 시작하겠습니까? 다시 시작 : 1, 종료 : 2");
        Scanner scanner = new Scanner(System.in);
        char answer = scanner.next().charAt(0);
        if(answer == '1'){
            return true;
        }
        return false;
    }
}

마찬가지로 Scanner클래스를 통해 다시 시작하는 여부를 입력받는다.

여기서는 한 글자를 받으므로 charAt()이라는 메서드를 사용해서 받은 글자를 char타입으로 변환해준다.

그리고 조건문을 이용하여 게임을 다시 시작할지 boolean타입으로 반환해준다.

 

 

 

 

 

게임 실행 부분 (MainGame 클래스)

package baseball.domain;

import java.util.List;

public class MainGame {
    public static void main(String[] args) {
        GenerateRandomNum randomNum = new GenerateRandomNum();
        Input input = new Input();
        Judge judge = new Judge();
        Playagain playagain = new Playagain();
        boolean again = true;

        while (again){
            List<Integer> computer = randomNum.create();
            String result = "";
            while (!result.equals("3스트라이크")){
                result = judge.judgement(computer, input.playerNumber());
                System.out.println(result);
            }
            again = playagain.playagain();
        }
    }
}

여태까지 각각의 클래스로 나누어 구현했던 부분을 객체로 불러와주고 

while조건문이 3 스트라이크가 나올 때까지 반복해준다.

게임을 다시 시작할 여부를 확인해주기 위해 boolean타입의 again변수를 하나 만들어 준 것이 특징이다.(위의 Playagain과 연결)

 

 


 

 

📖 단위 테스트

실제로는 각 클래스를 구현하고 바로 단위 테스트를 통해 확인해보았으나

글의 가독성을 위해 단위테스트 부분을 모아보았다.

해당 테스트는 JUnit5와 assertj를 사용하여 테스트하였다.

 

 

랜덤 3 자릿수 생성 단위 테스트

package baseball.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

class generateRandomNumTest {
    @Test
    @DisplayName("랜덤 숫자 생성 테스트")
    void randomTest(){
        GenerateRandomNum generateRandomNum = new GenerateRandomNum();
        List<Integer> test = generateRandomNum.create();
        assertThat(3).isEqualTo(test.size());
    }
}

정상적으로 랜덤 숫자 생성 확인

 

 

 

몇개의 숫자가 같은지, 스트라이크 수는 몇개인지 확인하는 단위테스트

package baseball.domain;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

class CompareTest {
    Compare compare;

    @BeforeEach
    void setUp(){
        compare = new Compare();
    }

    @Test
    @DisplayName("몇개의 숫자가 같은지 확인")
    void count(){
        assertThat(3).isEqualTo(compare.howMany(Arrays.asList(1,2,3), Arrays.asList(3,1,2)));
    }

    @Test
    @DisplayName("스트라이크 수 확인")
    void strike(){
        assertThat(1).isEqualTo(compare.countStrike(Arrays.asList(3,2,4),Arrays.asList(3,1,2)));
    }
}

정상적으로 total 즉 위치 상관없이 같은 숫자의 개수와 스트라이크 수 확인

 

최종 출력 확인 테스트

package baseball.domain;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class JudgeTest {
    Compare compare;
    Judge judge;

    @BeforeEach
    void setUp(){
        compare = new Compare();
        judge = new Judge();
    }

    @Test
    @DisplayName("3볼 확인")
    void ballcheak(){
        assertThat("3볼 0스트라이크").isEqualTo(judge.judgement(Arrays.asList(3,1,2),Arrays.asList(1,2,3)));
    }

    @Test
    @DisplayName("3스트라이크 확인")
    void strikecheck(){
        assertThat("0볼 3스트라이크").isEqualTo(judge.judgement(Arrays.asList(1,2,3),Arrays.asList(1,2,3)));
    }


    @Test
    @DisplayName("낫싱확인")
    void nothingcheck(){
        assertThat("낫싱").isEqualTo(judge.judgement(Arrays.asList(1,2,3),Arrays.asList(4,5,6)));
    }
}

정상적으로 Jundge클래스 구현 확인

 

 

 

 

 

이렇게 로직에 대하여 (UI(System.out, System.in) 로직은 제외)에 대해 단위 테스트를 구현해 보았다.

해당 프로그램을 만들 때 주의했던 점은

  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들기.
  • indent(인덴트, 들여 쓰기) depth를 2를 넘어가지 않도록 노력하기
  • 자바 코드 컨벤션을 지키면서 프로그래밍하기.

모든 부분이 완벽히 맞춰지지는 않았겠지만 많은 더 탄탄한 프로그램을 만들기 위해 많은 리팩터링을 경험할 수 있는 좋은 경험이었다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

복사했습니다!