들어가며.
@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_FORWARD 와 STOP 이 차례대로 반환된다는 것을 알 수 있습니다. 이렇게 저희가 임의로 랜덤값을 정해 테스트할 수 있게 되는 것이지요.
그럼 제가 이전에 작성한 테스트를 위 커스텀 라이브러리를 사용하여 테스트하도록 변경해보겠습니다.
@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;
}
}
이 코드는 필요가 없어지기 때문에 더 깔끔한 테스트 코드를 작성할 수 있게 됩니다.
마치며.
예전에는 이렇게 코드가 어떻게 동작하는지 확실히 알아내려고 하지 않았는데 프리코스를 진행하면서 새로운 재미를 찾아가는 것 같습니다. 저와 같이 프리코스 하시는 분들이 많을 거라 생각하는데 이 글이 이해하는데 도움되었으면 좋겠습니다.
'Trouble' 카테고리의 다른 글
[TEST] System.out 과 System.in 의 테스트 방법 (0) | 2024.10.24 |
---|---|
[Git] 첫 번째 커밋 삭제 시 reset 으로 삭제하면 안 되는 이유 (0) | 2024.08.17 |
[Lombok] @Builder 정적 가져오기 문제 (0) | 2024.08.17 |
[QueryDSL] 컬렉션 fetchJoin 페이징 applying in memory (0) | 2024.08.08 |