우테코 프리코스 2주차 자동차 경주 미션을 하면서 콘솔 입력 테스트를 공부하고 문제해결한 과정을 정리해봤습니다.
package camp.nextstep.edu.missionutils;
import java.util.Scanner;
public class Console {
private static Scanner scanner;
private Console() {
}
public static String readLine() {
return getInstance().nextLine();
}
public static void close() {
if (scanner != null) {
scanner.close();
scanner = null;
}
}
private static Scanner getInstance() {
if (scanner == null) {
scanner = new Scanner(System.in);
}
return scanner;
}
}
미션을 하면서 입력을 받기 위해 우테코에서 제공하는 Console 클래스의 readLine() 메서드를 사용했습니다. 해당 메서드는 내부적으로 입력 스트림을 생성자에 Scanner 인스턴스를 생성해서 nextLine() 메서드를 통해 입력 값을 받아오는 것을 확인할 수 있습니다.
@Test
void 자동차이동횟수입력_테스트() {
// given
String input = "5";
System.setIn(new ByteArrayInputStream(input.getBytes()));
// when
int totalRaceCount = InputHandler.getTotalRaceCount();
// then
assertThat(totalRaceCount).isEqualTo(Integer.parseInt(input));
}
정수 형태의 문자열을 입력받아서 검증 후 반환하는 InputHandelr.getTotalRaceCount() 메서드를 위한 간단한 테스트코드입니다. 위의 코드에서 볼 수 있듯이 System.setIn(new ByteArrayInputStream(input.getBytes()));를 사용하여 입력 값을 세팅해줄 수 있습니다.
이전의 Console에서 스캐너의 인스턴스를 생성할 때 System.in을 넘겨주었습니다. 즉 Console 클래스를 통해 표준 입력 스트림(System.in)의 값을 읽어올 것이라는 건데요, 테스트 코드에서는 System.setIn을 이용해 원하는 값을 표준 입력 스트림에 세팅해줄 수 있습니다.
java.util.NoSuchElementException: No line found
at java.base/java.util.Scanner.nextLine(Scanner.java:1660)
at camp.nextstep.edu.missionutils.Console.readLine(Console.java:12)
at racingcar.InputHandler.getCarNames(InputHandler.java:10)
at racingcar.InputHandlerTest.자동차_이름_1명_입력_테스트(InputHandlerTest.java:40)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
위에서 작성한 테스트코드를 그대로 사용하면 하나의 테스트만 실행할 때는 문제 없이 작동하지만, 여러개의 테스트를 동시에 진행하려고 하니 위와 같이 NoSuchElementException: No line found와 같이 에러가 발생합니다.
@AfterEach
void tearDown() {
Console.close();
}
@Test
void 자동차이동횟수입력_테스트() {
// given
String input = "5";
System.setIn(new ByteArrayInputStream(input.getBytes()));
// when
int totalRaceCount = InputHandler.getTotalRaceCount();
// then
assertThat(totalRaceCount).isEqualTo(Integer.parseInt(input));
}
해당 오류를 해결하기 위해서는 위와 같이 각 테스트가 끝나고 Console.close()를 통해 생성했던 스캐너를 close 해야합니다.
만약 Console.close()를 하지 않으면 이전 스캐너가 닫히지 않은 상태로 다시 테스트를 실행할 때 이미 값을 소진한 이전 입력 스트림에서 값을 읽어오려고 하다가 NoSuchElemtentException: No line found가 발생합니다. 따라서 Console.close()로 스캐너를 닫아준 후 System.setIn()을 해줘야 의도한대로 입력을 할 수 있습니다.
입력에 대한 예외 처리를 테스트하는 과정에서 null 입력을 테스트하려고 처음에 아래와 같이 코드를 작성했습니다.
String givenName = null;
System.setIn(new ByteArrayInputStream(givenName.getBytes()));
위처럼 코드를 작성하니 givenName.getBytes()에서 NullPointerException일 발생해서 사용할 수 없었습니다.
다른 테스트 방법을 찾기 위해 아래의 우테코가 제공하는 command() 코드를 활용해서 테스트를 작성해봤습니다. 이 테스트의 작동 결과는 어떻게 될까요?
private void command(final String... args) {
final byte[] buf = String.join("\n", args).getBytes();
System.setIn(new ByteArrayInputStream(buf));
}
@Test
void 자동차이름입력_예외테스트_입력없음(String input) {
// give
String givenName = null;
command(null);
// when & then
assertThatThrownBy(InputHandler::getCarNames)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("경주할 자동차 이름을 입력하세요.");
}
이번에는 예상과 달리 IllegalArgumentException이 발생하지 않았습니다. 즉 예상과 달리 null 값이 입력되지 않은 것이죠.
엇 그럼 null 대신 어떤 값이 입력된걸까요? 궁금증을 해결하기 위해 아래처럼 테스트를 실행시켜봤습니다.
assertThat(String.join("\n", input)).isEqualTo(null); // 1
assertThat(String.join("\n", input)).isEqualTo(""); // 2
assertThat(String.join("\n", input)).isEqualTo("null"); // 3
3개의 테스트 중 어느 것이 통과됐을까요..?
두구두구두구
...
..
.
정답은 3번이었습니다.
즉 테스트를 할 때 입력값으로 null을 넘겨 주어도 이 값이 null로 입력되는게 아니라 그냥 문자열 취급이 되는 것이었죠. 이 과정을 통해서 콘솔 입력 테스트를 할 때 null 입력은 배제한다는 결론을 내렸습니다. 실제 사용자가 null을 입력하는건 사실상 불가능하기도 한 점과 연결이 된다고 생각합니다.