[우테코 6기 프리코스] NsTest 코드 분석해보기 - 1탄: 맛보기

별의개발자커비·2023년 10월 9일
0

우테코 도전기

목록 보기
14/37
post-thumbnail

(23.10.23 추가)

해당 포스팅보다 정리된 설명인 아래의 포스팅을 참고해주세요!
https://velog.io/@dlguswl936/우테코-6기-프리코스-우테코-제공-NsTest-들여다보기-2편

개요

우테코에서 자체적으로 제공하는 NsTest를 통해서 랜덤, 셔플 값 등을 조절해서 테스트할 수 있는데 그동안 그걸 잘 활용하지 못했던 것이 아쉬웠다. 따라서, NsTest와 junit 코드 속을 파고 들어보면서 실제 프리코스 때 사용해보기 위한 정리를 해보려고 한다!

테스트 코드를 속까지 파고들어 이해하고 싶어서, 해당 포스팅에서 지식의 층을 나눠 꼬리물기로 들어가는 방식이 딱 맞을 것 같아 적용해보려고 한다. 매우 깊이 들어감 주의🚨🚨

[ junit ] assertTimeoutPreemptively

assertTimeoutPreemptively(Duration timeout, Executable executable)

assertTimeoutPreemptively(Duration.ofSeconds(10L), () -> {})

입력값 주입, 출력값으로 테스트


아래 부분을 통해 입력을 제어하고 출력값을 테스트 할 수 있었다. 코드 속으로 들어가면서 살펴보자.

final Executable executable = () -> {
    runException("4");
    assertThat(output()).contains("4");
};

1F: Executable 타입

실행 인터페이스로 Executable 인터페이스의 로직을 정의해서 쓴다.

Executable은 JUnit5에 정의되어 있는 함수형 인터페이스이다.
Runnable을 예외를 던질 수 있도록 재정의한 클래스
---중략---
함수를 실행하되 예외를 던질 수 있게 하도록 Executable을 사용한 것임을 알 수 있다.

2F: runException

  1. run(args) 로 넘겨주거나
  2. NoSuchElementException 예외가 발생하면 예외 발생시키지 않고 그냥 종료한다.

3F: run(args)

  • command(args)
  • runMain()

4F: command(args)

받은 string값을 System.in으로 입력값에 주입시켜준다.

4F: runMain()

가장 바깥의 사용자가 만든 테스트 클래스에서 overide해서 사용할 수 있게 추상 메소드가 만들어져있다. overide해서 여기에 실행시키고 싶은 클래스의 메소드를 넣어주면 된다.

3F: NoSuchElementException

2F: assertThat

output()).contains()

을 통해 출력값을 테스트 할 수 있다.

assertRandomTest

assertRandomTest(final Executable executable, final MyTest.Mocking... mockings)

일단 executable은 위에서 정의한 runMain()으로 실행되고,
MyTest.Mocking... mockings 부분은 살펴봐야겠다.

1F: try 절 (_)

final MockedStatic<Randoms> mock = mockStatic(Randoms.class)

이게 try의 () 부분에 있는데 이게 뭘까?

2F: MockedStatic<Randoms> mock

 final MockedStatic<Randoms> mock = mockStatic(Randoms.class)

Randoms을 MockedStatic이란 애가 감싸고 있는 변수가 있다. 얘는 Randoms의 클래스를 대상으로 mockStatic이라는 걸 실행해서 그 MockedStatic로 반환해주고 있다.

3F : mockStatic(Randoms.class

번역: 지정된 클래스 또는 인터페이스의 모든 static 메서드에 대한 스레드 로컬 mock 컨트롤러를 만듭니다.

4F: Mock 객체란?

이 포스팅에서 발췌해 온 부분으로 Mock을 이해할 수 있다.

⇒ 2F 정리

Randoms에 속한 모든 static 메소드에 대해서는, 임의로 조작하는 mocking을 만들어 사용할 것이다. 라고 이해할 수 있다.

2F: try 절 내의 mockings 변수

assertRandomTest(final Executable executable, final MyTest.Mocking... mockings)

assertRandomTest( executable, MyTest.Mocking.ofRandomNumberInRange(2, 0)
							, MyTest.Mocking.ofShuffle(List.of("김치찌개", "김밥")) );

현재 mockings에는 현재

  • 테스트 클래스MyTest
  • Mocking이라는 static 클래스의
  • static 메소드들인 ofRandomNumberInRange, ofShuffle가 들어가있다.

3F: new MyTest.Mocking()


그리고 ofRandomNumberInRange 와 같은 메소드는
new MyTest.Mocking()를 호출하고 있기 때문에
아래 구현되어있는 Mocking이라는 static 클래스를 따로 살펴봐야할 것 같다.

public static class Mocking<T> {}

1F: Mocking() 생성자


우선 생성자의 인자로 들어가있는 verification 부터 살펴봐야할 것 같다.

2F: MockedStatic.Verification

Mocking(final MockedStatic.Verification verification,
            final T value,final T... values)
            
new MyTest.Mocking(() -> Randoms.pickNumberInRange(anyInt(), anyInt()),
            value, values)

구현 부분을 보면 verification 자리에는 람다식이 들어가있다.

( () -> Randoms.pickNumberInRange(anyInt(), anyInt() )

챗gpt의 설명을 이해해보자면 verification은,

모킹된 메서드인 pickNumberInRange가 호출될 때마다 인자로 정상적인 정수값으로 잘 들어왔는지 검증해주는 역할인 것 같다.

3F: anyInt()

모든 int 또는 null이 아닌 정수를 반환한다.

1F: try 절 내의 mocking.stub(mock)

try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
    Arrays.stream(mockings).forEach(mocking -> mocking.stub(mock));
    executable.execute();
}

// mockings
MyTest.Mocking.ofRandomNumberInRange(2, 0),
MyTest.Mocking.ofShuffle(List.of("김치찌개", "김밥"))

// public static class Mocking<T>
public <S> void stub(final MockedStatic<S> mock) {
    mock	
    // Mocking.ofRandomNumberInRange(2, 0) 객체가
    .when(verification)	
    // ?가 new에서 정의한 Randoms.pickNumberInRange(anyInt(), anyInt() 에 검증 통과하면 
    .thenReturn(value, Arrays.stream(values).toArray()); 
    // Randoms.pickNumberInRange의 실행결과로 입력하는 value를 반환해라
}

해당 포스팅을 참고했는데, 사실 완전히 이해가 가지는 않았다. 대충 특정 메소드를 실행해서 확인하는 게 아닌, 조금 더 작은 단위의 특정 동작을 수행하는지 확인하는 용도이다. 정도로 이해 했다.

@ 여기부터 주석으로 정리

⇒ assertRandomTest 정리

// 그러니까
assertRandomTest(final Executable executable, final MyTest.Mocking... mockings)

assertRandomTest( executable, MyTest.Mocking.ofRandomNumberInRange(2, 0)
							, MyTest.Mocking.ofShuffle(List.of("김치찌개", "김밥")) );
                            
                            
                            
public static MyTest.Mocking ofRandomNumberInRange(final Integer value,
    final Integer... values) {
    return new MyTest.Mocking(() -> Randoms.pickNumberInRange(anyInt(), anyInt()),
    value, values);
}
        
private final MockedStatic.Verification verification;


private Mocking(final MockedStatic.Verification verification,
    final T value,
    final T... values) {
    this.verification = verification;
    this.value = value;
    this.values = values;
}

그래서 적용해서 코드를 이해해보자면 MyTest.Mocking.ofRandomNumberInRange(2, 0) , MyTest.Mocking.ofShuffle(List.of("김치찌개", "김밥"))
여기서 인자로 받은 int나 List로 Mocking한 각 메소드별 Mock 객체를 가지고 forEach로 두 mock 객체를 하나씩 정의한 stub로 확인하고,
execute를 갖고 실행하겠다. 의 의미다.

내가 간단히 해본 결과

테스트 코드 전체

package menu;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mockStatic;

import camp.nextstep.edu.missionutils.Randoms;
import camp.nextstep.edu.missionutils.test.NsTest;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.mockito.MockedStatic;

public class MyTest extends NsTest {

    private static final Duration RANDOM_TEST_TIMEOUT = Duration.ofSeconds(10L);

    @DisplayName("전체 기능 테스트")
    @Nested
    class AllFeatureTest {
        @Test
        void 기능_테스트() {
            assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
                final Executable executable = () -> {
                    runException("4");
                    assertThat(output()).contains(
                        "2", "0", "김치찌개"
                    );
                };
                assertRandomTest(executable,
                    MyTest.Mocking.ofRandomNumberInRange(2, 0), // 숫자는 카테고리 번호를 나타낸다.
                    MyTest.Mocking.ofShuffle(   // 월요일
                        List.of("김치찌개", "김밥") // 제임스
                    )
                );
            });
        }
    }

    private static void assertRandomTest(
        final Executable executable,
        final MyTest.Mocking... mockings
    ) {
        assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
            try (final MockedStatic<Randoms> mock = mockStatic(Randoms.class)) {
                Arrays.stream(mockings).forEach(mocking -> mocking.stub(mock));
                executable.execute();
            }
        });
    }

    @Override
    protected void runMain() {
        new MyTestClass().main();
//        Application.main(new String[]{});
    }

    public static class Mocking<T> {

        /**
         * stubbing lambda verification 예시) () -> Randoms.pickNumberInList(anyList())
         */
        private final MockedStatic.Verification verification;

        // 반환할 첫 번째 값
        private final T value;

        /**
         * 첫 번째 값을 반환하고 나서 다음에 반환할 값들. 예를 들면, verification을 처음 실행하면 value를 반환하고 두 번째 실행하면 values[0]을
         * 반환한다.
         */
        private final T[] values;

        private Mocking(final MockedStatic.Verification verification,
            final T value,
            final T... values) {
            this.verification = verification;
            this.value = value;
            this.values = values;
        }

        public static MyTest.Mocking ofRandomNumberInRange(final Integer value,
            final Integer... values) {
            return new MyTest.Mocking(() -> Randoms.pickNumberInRange(anyInt(), anyInt()),
                value, values);
        }

        public static <T> Mocking ofShuffle(final List<T> value,
            final List<T>... values) {
            return new MyTest.Mocking(() -> Randoms.shuffle(anyList()), value, values);
        }

        public <S> void stub(final MockedStatic<S> mock) {
            mock.when(verification).thenReturn(value, Arrays.stream(values).toArray());
        }
    }
}

output만 확인 버전

random 기능 제외

random 기능 없이 output확인을 위한 코드만 하면 아래와 같다.

package blackJack;

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

import blackJack.domain.CardGenerator;
import blackJack.domain.Player;
import blackJack.domain.Players;
import blackJack.domain.PlayersWithCard;
import blackJack.domain.RandomCardGenerator;
import blackJack.view.OutputView;
import camp.nextstep.edu.missionutils.test.NsTest;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;

public class PlayerWithCardTest extends NsTest {
    private static final Duration RANDOM_TEST_TIMEOUT = Duration.ofSeconds(10L);
    CardGenerator cardGenerator = new RandomCardGenerator();

    @Nested
    class AllFeatureTest {
        @Test
        void pwc() {
            assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> {
                runMain();
                assertThat(output()).contains(
                    "딜러", "가나", "다라", "스페이드", "일부러"
                );
            });
        }
    }

    @Override
    protected void runMain() {
        Players players = Players.fromTest(Player.fromTest("가나", 100)
            , Player.fromTest("다라", 100));
        PlayersWithCard playersWithCard = players.firstCardSetting(cardGenerator);
        OutputView.printFirstSetting(playersWithCard);
    }
}

0개의 댓글