해당 포스팅보다 정리된 설명인 아래의 포스팅을 참고해주세요!
https://velog.io/@dlguswl936/우테코-6기-프리코스-우테코-제공-NsTest-들여다보기-2편
우테코에서 자체적으로 제공하는 NsTest를 통해서 랜덤, 셔플 값 등을 조절해서 테스트할 수 있는데 그동안 그걸 잘 활용하지 못했던 것이 아쉬웠다. 따라서, NsTest와 junit 코드 속을 파고 들어보면서 실제 프리코스 때 사용해보기 위한 정리를 해보려고 한다!
테스트 코드를 속까지 파고들어 이해하고 싶어서, 해당 포스팅에서 지식의 층을 나눠 꼬리물기로 들어가는 방식이 딱 맞을 것 같아 적용해보려고 한다. 매우 깊이 들어감 주의🚨🚨
assertTimeoutPreemptively(Duration timeout, Executable executable)
assertTimeoutPreemptively(Duration.ofSeconds(10L), () -> {})
먼저 테스트를 실행하고 테스트가 timeout된 순간 순간 테스트를 종료시키는 메소드이다.
참고글 : [JUnit5] 테스트에 timeout 걸기(assertTimeout, assertTimeoutPreemptively)
아래 부분을 통해 입력을 제어하고 출력값을 테스트 할 수 있었다. 코드 속으로 들어가면서 살펴보자.
final Executable executable = () -> {
runException("4");
assertThat(output()).contains("4");
};
실행 인터페이스로 Executable 인터페이스의 로직을 정의해서 쓴다.
Executable은 JUnit5에 정의되어 있는 함수형 인터페이스이다.
Runnable을 예외를 던질 수 있도록 재정의한 클래스
---중략---
함수를 실행하되 예외를 던질 수 있게 하도록 Executable을 사용한 것임을 알 수 있다.
run(args)
로 넘겨주거나NoSuchElementException
예외가 발생하면 예외 발생시키지 않고 그냥 종료한다.받은 string값을 System.in으로 입력값에 주입시켜준다.
가장 바깥의 사용자가 만든 테스트 클래스에서 overide해서 사용할 수 있게 추상 메소드가 만들어져있다. overide해서 여기에 실행시키고 싶은 클래스의 메소드를 넣어주면 된다.
output()).contains()
을 통해 출력값을 테스트 할 수 있다.
assertRandomTest(final Executable executable, final MyTest.Mocking... mockings)
일단 executable은 위에서 정의한 runMain()으로 실행되고,
MyTest.Mocking... mockings 부분은 살펴봐야겠다.
final MockedStatic<Randoms> mock = mockStatic(Randoms.class)
이게 try의 () 부분에 있는데 이게 뭘까?
MockedStatic<Randoms> mock
final MockedStatic<Randoms> mock = mockStatic(Randoms.class)
Randoms을 MockedStatic이란 애가 감싸고 있는 변수가 있다. 얘는 Randoms의 클래스를 대상으로 mockStatic이라는 걸 실행해서 그 MockedStatic로 반환해주고 있다.
번역: 지정된 클래스 또는 인터페이스의 모든 static 메서드에 대한 스레드 로컬 mock 컨트롤러를 만듭니다.
이 포스팅에서 발췌해 온 부분으로 Mock을 이해할 수 있다.
Randoms에 속한 모든 static 메소드에 대해서는, 임의로 조작하는 mocking을 만들어 사용할 것이다. 라고 이해할 수 있다.
assertRandomTest(final Executable executable, final MyTest.Mocking... mockings)
assertRandomTest( executable, MyTest.Mocking.ofRandomNumberInRange(2, 0)
, MyTest.Mocking.ofShuffle(List.of("김치찌개", "김밥")) );
현재 mockings에는 현재
MyTest
의 Mocking
이라는 static 클래스의 ofRandomNumberInRange
, ofShuffle
가 들어가있다.
그리고 ofRandomNumberInRange
와 같은 메소드는
new MyTest.Mocking()를 호출하고 있기 때문에
아래 구현되어있는 Mocking
이라는 static 클래스를 따로 살펴봐야할 것 같다.
public static class Mocking<T> {}
우선 생성자의 인자로 들어가있는 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가 호출될 때마다 인자로 정상적인 정수값으로 잘 들어왔는지 검증해주는 역할인 것 같다.
모든 int 또는 null이 아닌 정수를 반환한다.
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(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());
}
}
}
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);
}
}