![article thumbnail image](https://blog.kakaocdn.net/dn/lOJmb/btrfnlu8RDR/F6gM0PlNYVZRpkGaTmqwG1/img.png)
- 이 글은 자바지기(박재성)님의 강의를 개인적인 공부를 위해 정리한 글입니다.
- 우아한테크세미나_TDD 리팩토링
단위 테스트 실습 - 숫자야구게임
- 다음 요구사항을 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)));
}
}
최종 출력 확인 테스트
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)));
}
}
이렇게 로직에 대하여 (UI(System.out, System.in) 로직은 제외)에 대해 단위 테스트를 구현해 보았다.
해당 프로그램을 만들 때 주의했던 점은
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들기.
- indent(인덴트, 들여 쓰기) depth를 2를 넘어가지 않도록 노력하기
- 자바 코드 컨벤션을 지키면서 프로그래밍하기.
모든 부분이 완벽히 맞춰지지는 않았겠지만 많은 더 탄탄한 프로그램을 만들기 위해 많은 리팩터링을 경험할 수 있는 좋은 경험이었다.
'JAVA with TDD, Clean Code' 카테고리의 다른 글
JAVA(자바) 단위 테스트 실습 - 문자열 계산기 (0) | 2021.09.03 |
---|---|
간단한 클래스 분리 (0) | 2021.08.21 |
JUnit, AssertJ, 단위테스트 개념 및 다양한 활용법 (0) | 2021.08.04 |
Set Collection에 대한 학습 테스트 (0) | 2021.07.30 |
String 클래스에 대한 학습 테스트 (0) | 2021.07.29 |