본문 바로가기
Trouble

[TEST] 우아한 프리코스 테스트 코드 알아보기

by 되고싶은노력가 2024. 10. 26.

 

들어가며.

@Test
void 기능_테스트() {
    assertRandomNumberInRangeTest(
        () -> {
            run("pobi,woni", "1");
            assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
        },
        MOVING_FORWARD, STOP
    );
}

@Test
void 예외_테스트() {
    assertSimpleTest(() ->
        assertThatThrownBy(() -> runException("pobi,javaji", "1"))
            .isInstanceOf(IllegalArgumentException.class)
    );
}

 

우아한 프리코스에서 제공하는 라이브러리를 사용하여 테스트를 진행하는 과정에서 어떻게 커스텀 메소드가 이루어져있는지 확인하고자 작성하게 되었습니다. 위 코드는 AssertJ 에서 제공하는 것이 아닌 import static 으로 Assertions 를 가져와 메소드를 작성한 것으로 보입니다.


# assertSimpleTest

우선 assertSimpleTest 를 살펴보겠습니다. assertSimpleTest 메소드를 자세히 들어가보면 아래와 같은 코드를 확인할 수 있습니다.

public static void assertSimpleTest(final Executable executable) {
    assertTimeoutPreemptively(SIMPLE_TEST_TIMEOUT, executable);
}

 

assertTimeoutPreemptively() 메소드를 래핑한 메소드라는 것을 알 수 있습니다. assertTimeoutPreemptively() SIMPLE_TEST_TIMEOUT 만큼 지정된 시간 안에 excutable 함수가 실행되는지 검사하는 작업을 합니다.

 

Excutable JUnit 5 에서 제공하는 함수형 인터페이스로 실행 가능한 코드를 표현하는데 사용합니다. 실행 가능한 코드 외에도 예외를 던질 수 있게 함으로써 유연함을 제공하죠.

 

즉, 아래와 같이 asserSimpleTest 안에 실행 가능한 함수를 전달하게 되면 지정된 시간 안에 작업되면 성공, 넘기면 테스트가 실패하게 됩니다. 

@Test
void 이름_입력값_컬렉션_반환() {
    List<String> cars = inputParser.toList("pobi,woni");

    assertSimpleTest(() ->
            assertThat(cars.size()).as("cars size").isEqualTo(2)
    );

    assertSimpleTest(() ->
            assertThat(cars).contains("pobi", "woni")
    );
}

 

또한 예외를 던지는 식으로도 가능합니다.

@Test
void 이름_사이에_공백이_존재하는_경우() {
    assertSimpleTest(() ->
            assertThatExceptionOfType(IllegalArgumentException.class)
                    .isThrownBy(() -> inputParser.toList("pobi,wo ni"))
    );

    assertSimpleTest(() ->
            assertThatIllegalArgumentException()
                    .isThrownBy(() -> inputParser.toList("pobi,wo ni"))
    );

    assertSimpleTest(() ->
            assertThatThrownBy(() -> inputParser.toList("pobi,wo ni"))
                    .isInstanceOf(IllegalArgumentException.class)
    );
}

# assertRandomNumberInRangeTest

assertion 을 이해하는데 꽤나 애를 먹었는데요. 하나씩 보면서 정리하도록 하겠습니다.

@Test
void 기능_테스트() {
    assertRandomNumberInRangeTest(
        () -> {
            run("pobi,woni", "1");
            assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
        },
        MOVING_FORWARD, STOP
    );
}

 

처음 코드를 보면 아까 위에서 설명드린 코드와 비슷한 부분이 있을겁니다. 함수를 던져주고 있다는 것인데요. 또 아래 상수를 보시면 어떠한 값이 같이 넘어가는 것이라는 걸 알 수 있습니다. 그럼 이부분에서 예측할수 있는 것은 어떠한 값에 의해서 위 함수가 동작하는 것이라고 생각할 수 있습니다.

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

 

해당 메서드를 타고 들어가면 다시 한번 어떠한 메서드로 매개변수로 넘어가는데요. 먼저 매개변수를 보자면 실행 가능한 함수 executable 과 어떠한 정수 value, 그리고 가변 인자인 values 입니다. 가변 인자는 여러 값을 전달하거나 아무 값도 전달하지 않을 수 있다는 특징이 있습니다.

 

assertRandomTest() 메서드에서는 다시 한번 새로운 함수 Randoms.pickNumberInRange() 와 이전에 넘어온 함수 executable 을 다시 넘겨주고 있습니다. 여기서 aniInt()"어떤 정수 값이든 매칭할 수 있다" 의 의미를 갖는다 정도만 알면 될 것 같습니다. 

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();
        }
    });
}

 

위와 같이 assertRandomTest() 를 살펴보겠습니다. 이번엔 mockStatic 이라는 새로운 메서드를 확인할 수 있는데요. 이 mockStatic 의 설명을 살펴보면 주어진 클래스의 모든 정적 메서드에 대한 모의 객체를 생성한다고 되어 있습니다.

 

이를 통해 정적 메서드를 호출을 제어하고 반환 값을 제어하거나 호출 여부를 확인할 수 있는데, 여기서는 Random 클래스를 모의 객체로 생성하고 있습니다.

 

생성된 mock 으로 when, 즉, Randoms.pickNumberInRange() 호출이 일어날 때 thenReturn() 안에 있는 value 와 가변 변수 values 가 순차적으로 반환되도록 설정된 것입니다.

 

마지막으로 executable, 저희가 처음 넘겼던 원하는 테스트가 실행되면서 when 에 정의된 메서드가 발생할 때, 반환값이 MOVING_FORWARDSTOP 이 차례대로 반환된다는 것을 알 수 있습니다. 이렇게 저희가 임의로 랜덤값을 정해 테스트할 수 있게 되는 것이지요.

 

그럼 제가 이전에 작성한 테스트를 위 커스텀 라이브러리를 사용하여 테스트하도록 변경해보겠습니다.

@ParameterizedTest
@ValueSource(ints = {4, 5, 6, 7, 8, 9})
void 자동차가_전진한_경우(int number) {
    final Car car = new Car("pobi", new TestNumberGenerator(number));
    car.move();
    assertThat(car.toString()).isEqualTo("pobi : -");
}

 

NumberGenerator 라는 인터페이스 클래스를 상속받아 테스트를 하는 방식이었는데요.

Random 값 자체를 테스트하는게 아닌 전진 유무에 대한 테스트이기 때문에 굳이 임의로 작성한 TestNumberGenerator 를 넘겨주지 않아도  assertRandomNumberInRangeTest() 를 사용하는 방식으로 변경할 수 있습니다.

@ParameterizedTest
@ValueSource(ints = {4, 5, 6, 7, 8, 9})
void 자동차가_전진한_경우(int number) {
    assertRandomNumberInRangeTest(
            () -> {
                final Car car = new Car("pobi", new RandomGenerator());
                car.move();
                assertThat(car.toString()).isEqualTo("pobi : -");
            },
            number
    );
}

 

어떤가요? 별차이 없어 보인다고요? 물론 이 부분만 본다면 별 차이가 없지만 제가 랜덤값을 테스트하기 위해 구현한

static class TestNumberGenerator implements NumberGenerator {
    private final int number;

    public TestNumberGenerator(int number) {
        this.number = number;
    }

    @Override
    public int generator() {
        return number;
    }
}

 

이 코드는 필요가 없어지기 때문에 더 깔끔한 테스트 코드를 작성할 수 있게 됩니다.


마치며.

예전에는 이렇게 코드가 어떻게 동작하는지 확실히 알아내려고 하지 않았는데 프리코스를 진행하면서 새로운 재미를 찾아가는 것 같습니다. 저와 같이 프리코스 하시는 분들이 많을 거라 생각하는데 이 글이 이해하는데 도움되었으면 좋겠습니다.