-
TDD 기반 야구 게임 개발하기 2. 게임 결과 계산기 만들기24년 11월 이전/레거시-야구 게임 (Feat. TDD) 2018. 8. 3. 15:38반응형
* 먼저 이 프로젝트의 지적 자산은 '코드 스쿼드'에 있음을 밝힙니다. 필자의 프로젝트의 소스코드는 https://github.com/gurumee92/baseballtdd/ 에 있습니다.
테스트 주도 개발 기반 야구 게임 만들기
2단계. 첫 기능 게임 결과 계산기 만들기
0. 들어가기에 앞서....
이전 포스팅에서는 4가지의 기능을 분류하였는데 그 기능들은 다음과 같습니다.
- 랜덤 숫자열 생성하기
- 사용자 입력 숫자열 생성하기
- 이 두개의 숫자열 비교하기
- 비교한 결과 출력하기
그런데 왜 숫자열들을 비교한 것일까요? 이유는 게임의 결과를 반환하기 위해서입니다. 비교하는 행위 자체가 게임의 결과를 계산하기 위한 행동이지요. 따라서 이전에 분류했던 숫자 비교기를 이제부터는 결과 계산기라고 명명하겠습니다.
1. 테스트는 가장 쉬운 것부터!
제가 생각하는 TDD 개발은 항상 제일 쉬운 테스트를 먼저 진행하는 것입니다. 기능 테스트 역시 제일 쉬운 것부터 진행합니다. 그래서 제가 생각한 제일 쉬운 기능은 2개의 숫자열들을 비교해서 게임 결과를 반환하는 기능이지요. 예를 들어서 만약 랜덤 생성기가 "123"을 사용자 입력이 "123"일 때 3개의 스트라이크 0개의 볼을 반환해주는 기능입니다.
잠깐! 먼저 기능 구현의 순서가 바뀐거 같은데?
야구 게임의 흐름은 다음과 같습니다.
- 3개의 랜덤 숫자열을 받습니다.
- 3개의 숫자열을 만들기 위햇 사용자 입력을 받습니다
- 두개의 숫자열들을 비교하여 결과를 계산합니다.
- 결과를 출력합니다.
- 스트라이크가 3개가 될 때까지 2~4를 반복합니다.
어떤 분들은 그럼 숫자열들을 비교하기 위해선 랜덤 숫자 생성기나 사용자 입력기가 먼저 구현되어야 하는거 아니야? 라고 생각하실 수도 있을 것 같습니다. 저도 처음에 그래야 하나라고 생각했거든요. 그런데 잘 살펴보면 숫자열들을 비교하는건 위 두 기능이 먼저 구현될 필요는 없습니다. 왜냐하면 예를 들어서, 저는 숫자열들을 String 타입으로 생성시킬겁니다. 결국 랜덤 숫자열 생성기나 사용자 입력기는 이 게임에서 1~9까지 수 중 서로 다른 3개의 수로 이루어진 String 타입을 반환하죠. 그리고 결과 계산기는 말 그대로 이 반환된 String들로만 결과를 계산합니다. 따라서 자신이 정한 적절한 입력값 형식을 만들어주면 이 기능을 위의 기능들의 구현 없이 테스트할 수 있단 얘기죠. 자 이제 본격적으로 결과 계산기를 구현해봅시다.
2. 기능 선정 후 단위 테스트 목록 작성하기
이제 기능 테스트를 하기 위해서는 각각의 단위테스트가 필요할 것 같습니다. 우리가 필요한 테스트 목록은 무엇일까요? 제가 정의한 결과 계산기의 테스트 목록은 다음과 같습니다.
- 2개의 숫자열(문자열)을 통해 스트라이크가 몇 개인지 계산할 수 있는가?
- 2개의 숫자열(문자열)을 통해 볼이 몇 개인지 계산할 수 있는가?
그리고 위 목록들은 또 각각 3개의 세부 테스트를 진행할 예정입니다. 가장 쉬운것부터 점점 구체화되는 테스트라는 것을 알 수 있을겁니다.
- "123", "123" 비교
- "123"과 변하는 문자열 비교
- 2개의 변하는 문자열 비교
이 정도의 단위 테스트를 모두 성공한다면 기능이 적절하게 돌아갈 것이라 생각합니다. 자 이제 본격적인 개발에 들어갑시다.
3. 본격적인 테스트 주도 개발
1. 시작은 언제나 클래스 생성 테스트부터!
클래스 생성 테스트는 Game클래스를 만드는 과정을 똑같이 따라합니다. 지루하긴 하겠지만 잘 따라해주세요. TDD는 일종의 개발 훈련이라고도 말할 수 있습니다. 클린 코드 작성을 위해 반복적인 작업을 몸에 배게 만드는 과정인거죠. 게임 결과 계산 테스트이니 GameResultCalculatorTest라고 명명하면 적당할 것 같군요. 원래는 기능마다 패키지를 만들어서 클래스들을 모아두는게 정석이지만 클래스 수가 적으니 패키지 하나에 몰아넣고 개발하겠습니다. default 패키지에 이 클래스를 만들어주세요.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Test;
public class GameResultCalculatorTest {
@Test
public void nothing(){
}
}역시 nothing부터 시작합니다. 여기서 Run을 하면 역시 통과 될 것입니다. TDD에서는 절대로 "아 이건 그냥 되겠지 넘어가" 라는 마인드를 버리셔야 합니다. 철저하게 테스트 주기를 거친 후에 소스 코드를 작성하는 것이 포인트입니다. 이제 깃에 올려주세요 코멘트 메세지는 간단하게 "GameResultCalculatorTest 생성" 정도로 하죠. 이제 결과 계산기를 만들어보죠. Test 코드를 다음과 같이 바꿔주세요.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Test;
public class GameResultCalculatorTest {
@Test
public void gameResultCalculatorCreateAvailable(){
GameResultCalculator calculator = new GameResultCalculator();
}
}이러면 또 실패하겠죠? 실패를 성공시키기 위한 가장 간단한 테스트는 무엇일까요? 바로 그 클래스를 만드는 것이겠죠? 이제 소스코드에 GameResultCalculator를 만들어 줍시다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
}이제 테스트를 진행하면 테스트가 성공할 겁니다. 성공했으니 리팩토링할 차례네요. 아직까지는 소스 코드 내 중복된 곳이 없습니다. 바로 다음으로 넘어가죠. GameResultCalculator 역시 게임에 종속된 기능이기 때문에 게임에서 정한 길이를 벗어날 수 없습니다. GameResultCalculator의 기능은 정확하게 말하면 두개의 문자열을 입력받아 정해진 길이만큼 스트라이크와 볼의 개수를 계산하는 것입니다. 따라서 게임 내 정해진 길이로 생성이 가능해야 하죠. 테스트 코드를 다음과 같이 변경해주세요.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Test;
public class GameResultCalculatorTest {
@Test
public void gameResultCalculatorCreateAvailable(){
GameResultCalculator calculator = new GameResultCalculator();
}
@Test
public void gameResultCalculatorCreateAvailableWhenMaxLenIsGameDefaultLength(){
GameResultCalculator calculator = new GameResultCalculator(Game.DEFAULT_LEN);
}
}이제 테스트를 진행해보면 실패합니다. 길이를 파라미터로 받는 생성자가 없기 때문이죠. 이제 테스트를 성공시키기 위해 최소한의 코드를 작성해볼 차례입니다. 당연히 저번처럼 maxLen 필드와 2개의 생성자를 정의하면 되겠죠?
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
private int maxLen;
public GameResultCalculator(){
this(Game.DEFAULT_LEN);
}
public GameResultCalculator(int maxLen){
this.maxLen = maxLen;
}
}이제 테스트를 진행해보면 통과할 것입니다. 성공했으니 리팩토링 단계를 진행해보죠. 이제 테스트 클래스 내에서 중복된 곳이 존재하니까 리팩토링을 해서 중복된 곳을 제거해주도록 하죠. 저번처럼 @Before + setUp으로 바꾸겠습니다.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
public class GameResultCalculatorTest {
private GameResultCalculator calculator;
@Before
public void setUp(){
calculator = new GameResultCalculator(Game.DEFAULT_LEN);
}
}그리고 한가지 더 GameResultCalculator는 길이가 주어져야 생성시킬 수 있는 아이입니다. 따라서 디폴트 생성자가 외부에 공개될 필요가 없죠. 따라서 디폴트 생성자를 private으로 바꾸겠습니다. 이후 모든 기능들을 만들었을 때조차 이 메소드가 쓰이지 않는다면 제거해버리면 됩니다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
private int maxLen;
private GameResultCalculator(){
this(Game.DEFAULT_LEN);
}
public GameResultCalculator(int maxLen){
this.maxLen = maxLen;
}
}이제 생성에 관련된 테스트는 끝난 것 같습니다. 바로 Git에 올려두도록 하죠. 커멘트는 GameResultCalculator 생성 테스트 정도로 해두죠.
2. 스트라이크 개수 계산 테스트
본격적으로 스트라이크를 계산해보죠. 일단 2개의 문자열을 받는 것이 확실합니다. TDD에 따르면 가장 쉬운 것부터 테스트를 해볼 것은 권장합니다. 제일 쉬운 테스트는 무엇일까요? 아무래도 스트라이크 3개를 체크하는 것이겠죠. 제일 쉬운 테스트인 "123", "123"을 비교하도록 하겠습니다. 따라서 테스트 코드를 다음과 같이 작성하시면 됩니다.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전 단계 동일
@Test
public void isCounted3StrikesWhen123And123(){
assertEquals(calculator.getStrikesCnt("123", "123"), 3);
}
}여기서 assertEquals 메소드는 해당 메소드의 결과값이 뒤에 나온 3이랑 같은지 테스트하는 JUnit 메소드입니다. 컴파일조차 안될겁니다. 왜냐하면 저희는 getStrikesCnt를 만들지 않았기 때문이죠. 한 번 만들어볼까요?
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전 단계 동일
public int getStrikesCnt(String s, String s1) {
return 0;
}
}자 이제 테스트를 진행해봅시다. 실패할겁니다. 왜냐하면 위 메소드는 3을 반환하지 않으니까요. 이제 테스트를 통과하기 위해 최소한의 코드 작성은 무엇일까요? 바로 returna문에 0이 아닌 3을 넣는 거죠. 이제 테스트가 통과할겁니다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전 단계 동일
public int getStrikesCnt(String s, String s1) {
return 3;
}
}이제 테스트가 통과했으니 리팩토링을 진행해볼까요? 먼저 테스트 코드 내에서 "123"이 중복되네요 일단은 지역 변수로 빼도록 하죠.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전 단계 동일
@Test
public void isCounted3StrikesWhen123And123(){
String testStr = "123";
assertEquals(calculator.getStrikesCnt(testStr, testStr), 3);
}
}그리고 맘에 걸리는 것이 있습니다. 계산기 클래스 내 getStrikesCnt의 파라미터 이름들이 무엇을 뜻하는지 잘 표명하지 않지요? 파라미터 명을 무엇으로 해야 명확할까요? 정답은 없겠습니다만, 저는 generated, input으로 지정하겠습니다. 첫번째 인자는 생성기가 만들어낸 문자열을 뜻하고 두번째 인자는 사용자 입력에 의해 만들어진 문자열을 뜻하는 것이지요.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전 단계 동일
public int getStrikesCnt(String generated, String input) {
return 3;
}
}자 리팩토링도 마쳤으니까 Git에 올리도록 하죠. 코멘트는 "GameResultCalculator - getStrikesCnt 테스트 1. (123, 123) -> 3" 로 해두죠. 이제 조금 더 디테일한 테스트를 작성해보도록 하죠. 하나를 고정해놓고 하나를 바꿔가면서 1개의 스트라이크를 반환하는지 작성하는 것입니다. 테스트 셋은 다음과 같습니다.
123, 145 -> 1
123, 628 -> 1
123, 893 -> 1
테스트 코드에 다음 코드를 추가해주세요.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 스트라이크 1개 반환하기 (1 고정, 1 변합)
* 1개의 스트라이크는 3개의 숫자라고 가정할 때 3곳에서 일어난다. 그것을 표현한 테스트 셋이다.
* 123, 145 -> 1
* 123, 628 -> 1
* 123, 893 -> 1
*/
@Test
public void isCountedOneStrikeWhenFirstIsFixed(){
assertEquals(calculator.getStrikesCnt("123", "145"), 1);
assertEquals(calculator.getStrikesCnt("123", "628"), 1);
assertEquals(calculator.getStrikesCnt("123", "893"), 1);
}
}이제 테스트 메소드명에 무엇을 테스트 하는지 다 표현하기 어렵습니다. 그래서 테스트 셋에 관한 정보를 주석에 적어 놓은 것이죠. 한 번 테스트를 진행해 볼까요? 실패합니다. 이 테스트를 통과하기 위해선 어떻게 해야 할까요? 한 번 getStrikesCnt에 반환을 1로 바꿔볼까요? 그러면 위의 작성한 테스트 때문에 실패합니다. 두 테스트를 통과시키기 위해서는 다른 방법이 필요합니다. 제가 생각하기에 제일 쉬운 방법은 문자열이 같으면 3을 아니면 1을 반환하면 쉽게 바꿀 수 있을 것 같군요. 코드를 바꿔봅시다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int getStrikesCnt(String generated, String input) {
return (generated.equals(input)) ? 3 : 1;
}
}이제 두 테스트가 통과합니다. 이제 리팩토링을 할 차례입니다. 역시 "123"이 중복되네요 바꾸도록 합시다.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
@Test
public void isCountedOneStrikeWhenFirstIsFixed(){
String generated = "123";
assertEquals(calculator.getStrikesCnt(generated, "145"), 1);
assertEquals(calculator.getStrikesCnt(generated, "628"), 1);
assertEquals(calculator.getStrikesCnt(generated, "893"), 1);
}
}그리고 이전에 언급을 하지 않았는데 리팩토링 후 테스트가 또 통과하는지 확인해봐야 합니당! 이제 2개의 테스트를 통과했으니 Git에 올리도록 하죠. 코멘트는 음 "GameResultCalculator - getStrikesCnt 테스트 2. (123, ...) -> 1 (1Stike)" 라고 하죠. 바로 다음 테스트로 넘어가죠. 그 다음 쉬운 건 역시 하나가 고정되어 있고 문자열을 변환시키면서 2개의 스트라이크를 반환하는 것입니다. 테스트 셋은 다음 정도면 적당하겠네요
123, 125 -> 2
123, 183 -> 2
123, 723 -> 2
테스트 코드를 작성해볼까요?
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
// 이전과 동일
/* 스트라이크 2개 반환하기 (1 고정, 1 변합)
* 2개의 스트라이크는 3개의 숫자라고 가정할 때 3곳에서 일어난다. 그것을 표현한 테스트 셋이다.
* 123, 125 -> 2
* 123, 183 -> 2
* 123, 723 -> 2
*/
@Test
public void isCountedTwoStrikesWhenFirstIsFixed(){
String generated = "123";
assertEquals(calculator.getStrikesCnt(generated, "125"), 2);
assertEquals(calculator.getStrikesCnt(generated, "183"), 2);
assertEquals(calculator.getStrikesCnt(generated, "723"), 2);
}
}바로 테스트가 실패합니다. 점점 테스트가 복잡해져감에 따라서 이제는 getStrikesCnt를 간단한 변형으로는 쉽게 테스트를 통과시킬 수 없을 것 같습니다. 이제 어떻게 해야 할까요? 음 제일 쉬운 방법은 역시 정해진 길이만큼 문자열들의 자리의 값을 비교해서 같으면 개수를 올린 후 그 개수를 반환하면 될 것 같습니다. 이제 GameResultCalculator의 getStrikesCnt 매소드를 다음과 같이 바꿔주세요
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int getStrikesCnt(String generated, String input) {
int cnt = 0;
for (int i=0; i<maxLen; i++)
if (generated.charAt(i) == input.charAt(i))
cnt += 1;
return cnt;
}
}이제 테스트가 통과할겁니다. 이제 리팩토링을 할 차례입니다. 우선 코드 패턴의 중복이 있습니다. 스트라이크 1개 테스트와 2개 테스트는 기본적으로 하나가 고정되어 있고 3개가 변하면서 스트라이크 개수를 체크합니다. 이 패턴을 하나의 메소드로 추출합시다. 이름은 assertManyWhenFirstIsFixed 정도로 해두죠.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전 동일
private void assertManyWhenFirstIsFixed(String generated, String[] inputs, int strikesCnt) {
for (String input : inputs)
assertEquals(calculator.getStrikesCnt(generated, input), strikesCnt);
}
@Test
public void isCountedOneStrikeWhenFirstIsFixed(){
String[] inputs ={"145", "628", "893"};
assertManyWhenFirstIsFixed("123", inputs, 1);
}
@Test
public void isCountedTwoStrikesWhenFirstIsFixed(){
String[] inputs = {"125", "183", "723"};
assertManyWhenFirstIsFixed("123", inputs, 2);
}
}테스트 코드 내에서 중복되는 메소드는 해당 메소드들 위에 올려준다는 것을 기억해주세요! 그리고 리팩토링하면서 한 가지 주의하면서 코딩할 점은 이번 프로젝트는 프로그래밍 제약 사항이 있다는 것입니다. 소스코드 그리고 최대한 테스트 코드 역시 다 만족시키는지 체크하면서 코딩해주세요. 물론 지금까지 모든 코드는 다 만족시키고 있습니다!
- 코드 10줄 이내인가? O
- 메소드 1가지 일만 하는가? O(strike만 계산합니다.)
- indent가 2이하인가? O (for - if)
- 전역 변수 사용이 없는가? O
이제 리팩토링까지 무사히 마쳤으니 Git에 올리도록 하죠. 코멘트는 "GameResultCalculator - getStrikesCnt 테스트 3. (123, ...) -> 2 (2Stike)"로 하죠. 이제 테스트 셋을 전부 변화시키는 테스트를 해보겠습니다. 스트라이크 개수 세기의 최종 테스트라도고 볼 수 있겠군요. 테스트 셋은 다음 정도로 하면 될 것 같습니다.
123, 456 -> 0
456, 478 -> 1
198, 297 -> 1
357, 467 -> 1
124, 125 -> 2
239, 259 -> 2
192, 392 -> 2
198, 198 -> 3
자 테스트 코드를 만들어 볼까요? 메소드 명은 isCountedStrikesManySituation으로 하죠.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 스트라이크 최종 테스트. 많은 상황에서 잘 동작하는지 확인
* 123, 456 -> 0
* 456, 478 -> 1
* 198, 297 -> 1
* 357, 467 -> 1
* 124, 125 -> 2
* 239, 259 -> 2
* 192, 392 -> 2
* 198, 198 -> 3
*/
@Test
public void isCountedStrikesManySituation(){assertEquals(calculator.getStrikesCnt("123", "456"), 0);
assertEquals(calculator.getStrikesCnt("456", "478"), 1);
assertEquals(calculator.getStrikesCnt("198", "297"), 1);
assertEquals(calculator.getStrikesCnt("357", "467"), 1);
assertEquals(calculator.getStrikesCnt("124", "125"), 2);
assertEquals(calculator.getStrikesCnt("239", "259"), 2);
assertEquals(calculator.getStrikesCnt("192", "392"), 2);
assertEquals(calculator.getStrikesCnt("198", "198"), 3);
}
}무사히 통과합니다. 리팩토링 여지도 없으니 Git에 올리도록 하죠. 코멘트는 다음과 같습니다. "GameResultCalculator - getStrikesCnt 테스트 4. 최종"
3. Ball 개수 계산 테스트
ball 개수를 세는 메소드 역시 getStrikesCnt와 비슷한 테스트 목록을 지닙니다. 기능도 비슷하기 때문에 바로 구현해야지 하고 싶어도 TDD의 기본을 따라서 작성합시다..(사실 이렇게 하고 싶은 마음이 진짜 굴뚝 같습니다 흐어엉..) 볼 개수를 세는 제일 쉬운 테스트는 볼 개수가 0개일 때입니다. 123, 456일때는 볼 개수가 0개이겠군요. 바로 테스트 코드를 작성하죠.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
@Test
public void isCountedBallsWhen123And456(){
assertEquals(calculator.getBallsCnt("123", "456"), 0);}
}테스트는 실패합니다. getBallsCnt가 없기 때문이죠. 바로 계산기 클래스에 해당 메소드를 추가합시다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int getBallsCnt(String generated, String input) {
return 0;
}
}테스트가 통과됩니다. 리패토링할 여지도 없군요. Git에 올리도록 하죠. "GameResultCalculator - getSBallsCnt 테스트1. 123, 456 ->0"로 코멘트를 하고 넘어가죠. 바로 Ball이 1개일때를 테스트 해보도록 하죠. 메소드 명은 isCountedOneBallWhenFirstIsFixed 정도면 괜찮을 것 같습니다. 테스트 셋은 다음처럼 작성해봅시다.
123, 516 -> 1
123, 561 -> 1
123, 247 -> 1
123, 472 -> 1
123, 369 -> 1
123, 639 -> 1
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 볼 1개 반환하기 (1개 고정, 1개 변함)
* 123, 516 -> 1
* 123, 561 -> 1
* 123, 247 -> 1
* 123, 472 -> 1
* 123, 369 -> 1
* 123, 639 -> 1
*/
@Test
public void isCountedOneBallWhenFirstIsFixed(){
assertEquals(calculator.getBallsCnt("123", "516"), 1);
assertEquals(calculator.getBallsCnt("123", "561"), 1);
assertEquals(calculator.getBallsCnt("123", "247"), 1);
assertEquals(calculator.getBallsCnt("123", "472"), 1);
assertEquals(calculator.getBallsCnt("123", "369"), 1);
assertEquals(calculator.getBallsCnt("123", "639"), 1);
}
}테스트는 실패합니다. 이번에는 어떤 최소한의 코드를 작성하면 쉽게 문제를 해결할 수 있을까요? 마땅히 떠오르지 않습니다. 이럴 때 저는 편법으로 위의 getStrikesCnt를 참고합시다. 왜냐하면 2개의 메소드는 한개만 빼고 완전히 동일하기 때문이죠. 무엇이 다르냐면 개수를 세는 조건이 다릅니다. 스트라이크의 조건은 해당 자리의 숫자가 일치하는지 여부입니다. 그렇다면 볼의 조건은 무엇일까요? 볼의 조건은 바로 스트라이크는 아니면서 해당 숫자가 다른 문자열에 존재하는 경우지요. 이것을 참고하여 getBallsCnt 메소드를 다음과 같이 작성해봅시다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int getBallsCnt(String generated, String input) {
int cnt = 0;
for (int i=0; i<maxLen; i++)
if (!(generated.charAt(i) == input.charAt(i)) && generated.contains(input.charAt(i)+""))
cnt += 1;
return cnt;
}
}이제 테스트가 통과했으니 리팩토링할 차례군요. 우선 테스트 코드부터 바꿔봅시다. 전처럼 "123"과 숫자 1이 중복됩니다. 이를 지역 변수로 뺴주시면 됩니다.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
@Test
public void isCountedOneBallWhenFirstIsFixed(){
String generated = "123";
int ballsCnt = 1;
assertEquals(calculator.getBallsCnt(generated, "516"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "561"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "247"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "472"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "369"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "639"), ballsCnt);
}
}그리고 소스 코드 내에도 중복이 있군요. 바로 스트라이크인지 여부를 판단하는 부분입니다.
generated.charAt(i) == input.charAt(i)
이 부분을 메소드로 바꾸도록 하지요. 인덱스의 위치에서 스트라이크인지 판단하는 메소드니까 isStrikeAtIndex가 적당할 것 같군요. 다음과 같이 계산기 클래스를 작성하면 됩니다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int getStrikesCnt(String generated, String input) {
int cnt = 0;
for (int i=0; i<maxLen; i++)
if (isStrikeAtIndex(generated, input, i))
cnt += 1;
return cnt;
}
private boolean isStrikeAtIndex(String generated, String input, int index) {
return generated.charAt(index) == input.charAt(index);
}
public int getBallsCnt(String generated, String input) {
int cnt = 0;
for (int i=0; i<maxLen; i++)
if (!(isStrikeAtIndex(generated, input, i)) && generated.contains(input.charAt(i)+""))
cnt += 1;
return cnt;
}
}소스코드에서 private 메소드를 호출 시에 해당 메소드 아래에 인덴트 하는 것을 기억해주세요. 클린 코더스 영상에서는 다음처럼 하면 위에서 아래로 훑기 때문에 코드 가독성이 좋다고 하더라구요. 해당 위치에서 스트라이크 여부를 판단하는 조건을 메소드로 뺀 것처럼 볼 여부를 판단하는 조건 역시 메소드를 빼면 좋겠습니다. 마찬가지로 이름은 isBallAtIndex입니다. 소스 코드는 다음 처럼 변경됩니다.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int getBallsCnt(String generated, String input) {
int cnt = 0;
for (int i=0; i<maxLen; i++)
if (isBallAtIndex(generated, input, i))
cnt += 1;
return cnt;
}
private boolean isBallAtIndex(String generated, String input, int index) {
return !(isStrikeAtIndex(generated, input, index)) && generated.contains(input.charAt(index)+"");
}
}이제 테스트, 소스 코드 내에 더 이상 리팩토링 여지가 보이지 않군요. 이제 바로 올리겠습니다. 코멘트는 "GameResultCalculator - getBallsCnt 테스트2. 볼 1개인지 테스트" 라고 해두면 되겠습니다. 그리고 소스코드에 메소드가 추가되었으니 이들 정보도 있으면 좋겠습니다. 다음처럼 말이죠.
GameResultCalculator - getBallsCnt 테스트 2. 볼 1개인지 테스트
- isStrikeAtIndex : boolean 추가 - isBallAtIndex : boolean 추가
잠깐 여기서 중요한 것이 있습니다. generated.contains() 메소드는 문자열 전체에서 해당 문자열이 있는지 여부를 나타내는 메소드죠. 이는 로직 에러 요소를 갖고 있습니다. 예를 들어 "1234", "542"를 테스트해보면 원래는 제한된 길이 3 이내에서는 "123", "542"를 체크하니까 1볼이 나와야 정상입니다. 하지만 2볼이 나올 것 같군요. 여기를 테스트해볼까요? isBallCalculateInDifferentLenths 메소드를 만들어봅시다.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
@Test
public void isBallCalculateInDifferentLenths(){
assertEquals(calculator.getBallsCnt("1234", "542"), 1);
}
}예상 대로 실패합니다. 제한된 길이 내에서 판단하려면 어떻게 해야 할까요? 이때는 메소드를 빼서 for문을 한번 돌아도 되지만 String에 substring 메소드를 활용하면 더 쉽게 될 것 같습니다. 다음처럼 말이죠.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일private boolean isBallAtIndex(String generated, String input, int index) {
return !(isStrikeAtIndex(generated, input, index))
&& generated.substring(0, maxLen).contains(input.charAt(index)+"");
}
}이제 테스트를 다시 해볼까요? 마침 통과하는군요! Git에 다시 올리도록 하죠 "GameResultCalculator - getBallsCnt 테스트2-1. 볼 1개인지 테스트 isBallAtIndex 수정" 코멘트를 달아주세요. 이처럼 자신이 작성하지 않은 코드에 대해서는 더 세심하게 테스트할 필요가 있습니다. 이 점을 유의해주세요.
이제 볼 2개를 체크해볼까요? 메소드 명은 isCountedTwoBalslWhenFirstIsFixed, 테스트 셋은 다음처럼 작성하면 될 것 같습니다.
123, 234 -> 2
123, 342 -> 2
123, 315 -> 2
123, 351 -> 2
123, 216 -> 2
123, 612 -> 2
바로 테스트를 시작하죠.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 볼 2개 반환하기 (1개 고정, 1개 변함)
* 123, 234 -> 2
* 123, 342 -> 2
* 123, 315 -> 2
* 123, 351 -> 2
* 123, 216 -> 2
* 123, 612 -> 2
*/
@Test
public void isCountedTwoBalslWhenFirstIsFixed(){
String generated = "123";
int ballsCnt = 2;
assertEquals(calculator.getBallsCnt(generated, "234"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "342"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "315"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "351"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "216"), ballsCnt);
assertEquals(calculator.getBallsCnt(generated, "612"), ballsCnt);
}
}테스트가 무사히 통과합니다. 이제 리팩토링할 차례입니다. 역시 Strike 개수를 셀때와 마찬가지로 같은 패턴이 중복되는군요 이를 메소드로 빼두면 좋을 것 같습니다. 다음처럼 말이죠.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
//길이가 다를때 테스트
@Test
public void isBallCalculateInDifferentLenths(){
assertEquals(calculator.getBallsCnt("1234", "542"), 1);
}
private void assertManyBallsWhenFirstIsFixed(String generated, String[] inputs, int ballsCnt) {
for (String input : inputs)
assertEquals(calculator.getBallsCnt(generated, input), ballsCnt);
}
@Test
public void isCountedOneBallWhenFirstIsFixed(){
String[] inputs = {"516", "561", "247", "472", "369", "639"};
assertManyBallsWhenFirstIsFixed("123", inputs, 1);
}
@Test
public void isCountedTwoBallsWhenFirstIsFixed(){
String [] inputs = {"234","342", "315", "351", "216", "612" };
assertManyBallsWhenFirstIsFixed("123", inputs, 2);
}
}가독성을 위해 길이가 다를때를 위로 보내고 나머지 관련 부분들을 묶어주었습니다. 훨씬 보기 편하군요. 좋습니다. 바로 Git에 올리도록 하죠. 코멘트는 "GameResultCalculator - getBallsCnt 테스트3. 볼 2개인지 테스트" 하고 넘어갑시다.
이제 볼 3개를 잘 반환하는지 테스트해볼까요? 메소드명은 isCountedThreeBallsWhenFirstIsFixed로 하고 테스트 셋은 다음 정도면 괜찮을 것 같습니다.
123, 231 -> 3
123, 312 -> 3
바로 코드로 작성하죠!
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 볼 3개 반환하기 (1개 고정, 1개 변함)
* 123, 231 -> 2
* 123, 312 -> 3
*/
@Test
public void isCountedThreeBallsWhenFirstIsFixed(){
String [] inputs = { "231", "312" };
assertManyBallsWhenFirstIsFixed("123", inputs, 3);
}
}코드를 테스트해볼까요? 통과합니다. Git에 "GameResultCalculator - getBallsCnt 테스트4. 볼 3개인지 테스트" 라고 코멘트를 달아서 올리도록 합시다. 스트라이크일 때는 실패가 많았는데 볼 처리할때는 테스트 성공이 많군요. 왜냐하면 스트라이크 처리 시에 많은 부분이 테스트 처리를 하였고 그와 같은 로직을 조건 하나만 더 달아서 코드를 작성했기 때문에 많은 부분이 통과가 되는 것이지요. 이제 많은 상황에 대한 테스트를 해 볼까요? 이것 역시 볼 개수 세기에 대한 최종 처리라고 볼 수 있겠군요! isCountedBallsManySituation이라 이름 짓고 다음 테스트 셋을 테스트 해봅시다.
456, 789 -> 0
147, 753 -> 1
469, 341 -> 1
275, 129 -> 1
249, 928 -> 2
368, 637 -> 2
156, 562 -> 2
169, 916 -> 3
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 볼 개수 세기 최종
* 456, 789 -> 0
* 147, 753 -> 1
* 469, 341 -> 1
* 275, 129 -> 1
* 249, 928 -> 2
* 368, 637 -> 2
* 156, 562 -> 2
* 169, 916 -> 3
*/
@Test
public void isCountedBallsManySituation(){
assertEquals(calculator.getBallsCnt("456", "789"), 0);
assertEquals(calculator.getBallsCnt("147", "753"), 1);
assertEquals(calculator.getBallsCnt("469", "341"), 1);
assertEquals(calculator.getBallsCnt("275", "129"), 1);
assertEquals(calculator.getBallsCnt("249", "928"), 2);
assertEquals(calculator.getBallsCnt("368", "637"), 2);
assertEquals(calculator.getBallsCnt("156", "562"), 2);
assertEquals(calculator.getBallsCnt("169", "916"), 3);
}
}자 테스트를 통과합니다. 이제 볼에 대한 계산 테스트가 완료 되었습니다. 이제 이 기쁜 소식을 Git에 "GameResultCalculator - getBallsCnt 테스트 5. 최종" 코멘트를 달아 올립시다.
4. 계산 결과 반환 테스트
자 이제 계산기의 계산 결과 반환 기능을 테스트를 끝으로 계산 기능을 마무리 하겠습니다. 우선 제일 쉬운 테스트는 123, 123 -> [3, 0]입니다. 이게 정상적으로 작동하는지 보죠,
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
@Test
public void isCalculatedWell123And123(){
assertArrayEquals(calculator.calculateGameResult("123", "123"), new int[]{3, 0});}
}이 테스트는 컴파일이 되지 않습니다. 따라서 소스 코드를 작성하시면 됩니다. 다만 계산 결과를 반환하는 것은 여지껏 테스트했던 getStrikesCnt와 getBallsCnt를 이용하는 것이기 떄문에 이번에 바로 자신감 있게 작성해보도록 하죠.
[src/main/java/GameResultCalculator.java]
public class GameResultCalculator {
//이전과 동일
public int[] calculateGameResult(String generated, String input){
int strikes = getStrikesCnt(generated, input);
int balls = getBallsCnt(generated, input);
return new int[]{ strikes, balls };
}
}테스트를 통과하는지 확인해 봅시다. 성공이군요! Git에 "GameResultCalculator - calculateGameResult 테스트 1. 123, 123 -> [3, 0]" 이라는 코멘트를 달아서 올리도록 합시다. 이제 바로 계산 결과를 올바르게 반환하는지 최종 테스트를 진행하도록 하겠습니다. 우리는 테스팅이 잘 된 메소드를 반환만 하는 기능을 테스팅 하는거니까요! 테스트 셋은 다음이면 충분할 것 같습니다.
[src/test/java/GameResultCalculatorTest.java]
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
public class GameResultCalculatorTest {
//이전과 동일
/* 많은 상황에서 계산 결과 잘 반환하는지 테스팅
* 모든 결과 테스트 확인
* 123 123 -> (3, 0)
* 123 125 -> (2, 0)
* 453 123 -> (1, 0)
* 231 123 -> (0, 3)
* 432 123 -> (0, 2)
* 891 123 -> (0, 1)
* 132 123 -> (1, 2)
* 134 123 -> (1, 1)
* 123 456 -> (0, 0)
*/
@Test
public void isCalculatedWellManySituation() {
assertArrayEquals(calculator.calculateGameResult("123", "123"), new int[]{3, 0});
assertArrayEquals(calculator.calculateGameResult("123", "125"), new int[]{2, 0});
assertArrayEquals(calculator.calculateGameResult("453", "123"), new int[]{1, 0});
assertArrayEquals(calculator.calculateGameResult("231", "123"), new int[]{0, 3});
assertArrayEquals(calculator.calculateGameResult("432", "123"), new int[]{0, 2});
assertArrayEquals(calculator.calculateGameResult("891", "123"), new int[]{0, 1});
assertArrayEquals(calculator.calculateGameResult("132", "123"), new int[]{1, 2});
assertArrayEquals(calculator.calculateGameResult("134", "123"), new int[]{1, 1});
assertArrayEquals(calculator.calculateGameResult("123", "456"), new int[]{0, 0});
}
}자 모든 테스트가 통과합니다. 총 12개의 테스트를 무사히 통과하는 소스 코드를 얻게 되었습니다. Git에 "GameResultCalculator - calculateGameResult 테스트 최종, 클래스 완성" 이라는 코멘트를 달아서 올리도록 합시다.
4. 마치며....
이번 포스팅에서는 정의가 모호했던 "숫자 비교기"를 "게임 결과 계산기"라고 재정의해서 의미를 명확히 하였고 저번 포스팅에 비해서 더 깊은 단위 테스트 코드를 작성하였습니다. 다음 포스팅에는 1~9까지 무작위로 3개의 서로 다른 숫자들을 생성하는 랜덤생성기를 TDD로 진행해보겠습니다.
728x90'레거시 > 레거시-야구 게임 (Feat. TDD)' 카테고리의 다른 글
TDD 기반 야구 게임 개발하기 6. 야구 게임 마무리 (0) 2018.08.13 TDD 기반 야구 게임 개발하기 5. 게임 결과 출력기 (0) 2018.08.09 TDD 기반 야구 게임 개발하기 4. 사용자 입력 숫자열 생성기 (0) 2018.08.08 TDD 기반 야구 게임 개발하기 3. 랜덤 숫자열 생성기 (0) 2018.08.03 TDD 기반 야구 게임 개발하기 1. 프로젝트 개요 (0) 2018.08.02