티스토리 뷰

드디어 고대하던 우아한 프리코스가 시작됐다.

 

첫주차의 과제는 숫자 야구 게임으로 지난 기수의 첫주차 과제인 온보딩과 달리

시작부터 구현을 요구하는 과제로 진행됐다.

 

과제의 요구사항과 소스는 하단의 링크를 참조하면 되겠다.

 

https://github.com/0bliviat3/java-baseball-6

 

GitHub - 0bliviat3/java-baseball-6: 숫자 야구 미션을 진행하는 저장소

숫자 야구 미션을 진행하는 저장소. Contribute to 0bliviat3/java-baseball-6 development by creating an account on GitHub.

github.com

 

이번 포스팅에서는 과제를 진행하면서 겪은 시행착오와 공부한 부분들에 대해 적어보도록 하겠다.

 

먼저 과제를 진행하기전에도 CLI 프로그램은 몇번 만들어 본 경험이 있으므로 (ex. 타자게임)

단순 기능의 구현만이 완료된 돌아가기만 하는 녀석은 금방 만들어 낼 수 있었다.

 

그러나 분명 콘솔입력을 통한 셀프테스트에서는 잘 돌아가지만,

테스트 코드의 한가지 케이스를 계속 통과하지 못하는 상황이 발생했다.

 

직접 콘솔에 입력을 통해 테스트를 진행하는것과 달리 프리코스에서는

Mockito를 사용한 테스트 코드를 통해 테스트를 진행하는 방식을 사용하고 있었다.

 

이는 완전 처음보는 녀석이였고 처음 구현할때는 그냥 돌아가기만 한다면 테스트 코드도 자연스럽게

통과하지 않을까하는 안일한 생각으로 구현했던것이 처음의 실패 요인이였다.

 

테스트코드 하나가 통과하지 못하는 상황에서 해결해 나간 과정은

먼저 모든 소스를 좀 더 가독성 좋은 형태의 소스로 세분화해 다시 작성하는 것이였다.

요구사항의 흐름을 보자면,

 

사용자의 게임시작 -> 컴퓨터의 랜덤 숫자 생성 -> 

반복) 입력 -> 출력

 

다음과 같은 흐름으로 진행되므로 MVC 디자인 패턴을 적용해 설계해도 좋겠다란 생각이 들어

Controller, View, Util, Exception, Service 단으로 나누어 소스를 작성했다.

 

그렇게 다시 작성한 소스는 역시 같은 테스트 코드를 통과하지 못했고,

결국 테스트코드 소스를 뜯어봐야겠단 생각을 했다. 

 

실패한 테스트 코드는 바로

 

    @Test
    void 게임종료_후_재시작() {
        assertRandomNumberInRangeTest(
                () -> {
                    run("246", "135", "1", "597", "589", "2");
                    assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료");
                },
                1, 3, 5, 5, 8, 9
        );
    }

 

이 녀석이였는데 처음에 보고 사실 좀 당황하긴 했다.

 

assertRandomNumberInRangeTest라는 녀석이 인자로 함수와 임의의 숫자들을 갖고

인자로 받는 익명의 함수는 임의의 문자열을 인자로 넘기는 메소드를 호출하는 형태로

이제껏 사용해보지 않은 형태의 문법이였기 때문이다.

 

일단 assertRandomNumberInRangeTest라는 메소드를 호출 하고 있으므로

이 녀석을 한번 타고 가보기로 했다.

 

타고 들어가본 소스는 다음과 같다.

 

public static void assertRandomNumberInRangeTest(
        final Executable executable,
        final Integer value,
        final Integer... values
    ) {
        assertRandomTest(
            () -> Randoms.pickNumberInRange(anyInt(), anyInt()),
            executable,
            value,
            values
        );
    }

 

final 키워드를 사용한 인자로 Excutable,Integer 객체를 받아 내부에서 

assertRandomTest 메소드 호출시 파라미터로 넘겨주는 것을 확인 할 수 있었다.

 

또 assertRandomTest 메소드 역시 익명의 함수를 인자로 받아 프로그램 요구사항에서 명시되어 있던

Randoms.pickNumberInRange 메소드를 호출하고 있음을 알 수 있었다.

 

슬슬 조금은 뭔가 알것같은 간질간질한 느낌이 왔다.

 

그럼 조금 더 알기 위해 assertRandomTest 메소드를 타고 들어가 보기로 했다.

 

타고 들어간 소스는 다음과 같다.

 

private static <T> void assertRandomTest(
        final Verification verification,
        final Executable executable,
        final T value,
        final T... values
    ) {
        assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
            try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
                mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
                executable.execute();
            }
        });
    }

 

아 이번엔 뭔가 심상치 않은 녀석이 메소드에 포함되어 있어 보였다.

MockedStatic... 이 녀석은 검색의 힘이 좀 필요해 보인다. 

검색전 분석할수 있는 부분은 최대한 분석을 해보기로 했다.

 

먼저 주의깊게 본점은 이전에 파라미터로 계속 넘겨받은 값들을 현재 메소드에서 사용하고 있는 모습이다.

앞서 언급했던 Randoms.pickNumberInRange 메소드는 Verification과 대응되어 when()의 파라미터로

넘겨주었고, Integer 로 받아온 임의의 숫자값들은 value, values로 대응되어 Arrays의 스트림을 통해 

배열로 캐스팅되어 thenReturn 메소드의 인자값으로, 그리고 파라미터로 지속적으로 넘겨받은 

Excutable 객체는 execute 메소드를 호출하는것을 확인 할 수 있다.

 

이제 MockedStatic의 when, thenReturn 메소드가 어떤 역할을 하는것인지

검색을 통해 확인 해보았다.

 

검색을 통해 스스로 어느정도 정리가 된 부분은 다음과 같다.

테스트 코드를 위해 사용되는 Mockito 라는 녀석은 실제의 모듈을 사용하지 않고 가짜 모듈로

실제 모듈을 흉내(모킹)내서 테스트의 효율을 높이기 위해 사용되는것이다.

 

대략적인 흐름은 when, given, then 이 세단계의 흐름으로 진행되며,

given에서는 임의로 정한 테스트 상태를 주고

when에서는 given으로 받은 테스트 상태를 받아 테스트할 메소드를 실행시킨 뒤

then에서는 메소드의 결과값과 개발자가 설정한 임의의 값에 대한 결과값이 일치하는지

확인한다.

 

조금 더 자세한 정리는 추후에 다른 포스팅에서 다루도록 하고 다시

테스트 코드로 돌아가도록 하자.

 

다시 코드를 보자면,

 

try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
                mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
                executable.execute();
            }

 

when에서 Randoms.pickNumberInRange 메소드가 실행되는 시점에 모킹하고,

thenReturn 에서 내가 작성한 소스의 실행 결과값과 테스트코드에서 임으로 설정한 결과값이 일치한다면

통과하게 됨을 알 수 있었다.

 

따라서 이전에 작성한 로직인 배열 두개를 사용해 

길이 3배열로 모든 수를 관리하고, boolean 타입 길이 10 배열로

각각의 수의 포함 여부를 관리하며 이를 통해 랜덤값을 재귀호출로 생성하는 방식으로는 

제대로 모킹이 되지 않아 오류를 일으킨다는 결론이 나왔다.

 

이전에 작성한 코드)

 

public void setNumbers() {
        setNumbers(0);
    }

private void setNumbers(int idx) {
	
    if(idx == 3) { return; }
    int number = Randoms.pickNumberInRange(0, 8) + 1;
    if(numberFlag[number]) {
    	setNumbers(idx);
    } else {
        numberFlag[number] = true;
        numbers[idx] = number;
        setNumbers(idx + 1);
    }
}

 

 

새로 작성한 코드)

 

    public void setNumbers() {
        while (numbers.size() < 3) {
            int randomNumber = Randoms.pickNumberInRange(1, 9);
            if (!numbers.contains(randomNumber)) {
                numbers.add(randomNumber);
            }
        }
    }

 

소스의 수정은 그리 오래걸리지 않았다 숫자관리를 Util에서만 전담하고 있었기 때문에

메소드 하나의 수정과 멤버변수를 사용하는 메소드일부의 수정으로 충분히 해결이 가능했고,

수정이 끝난뒤 다시 실행시켰을땐 드디어 모든 테스트 코드를 통과하는것을 볼 수 있었다.

 

 


 

첫주차 과제부터 얻어가는것이 많아 역시 신청하길 잘했다란 생각이 들고 

공부할부분을 새로 알게 될 기회가 되어 재밌게 임할수 있었다.

다음 과제도 벌써 기대가 된다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함