본문 바로가기
우아한테크코스 4기/레벨1

[Java] JUnit으로 System.in, System.out 테스트하기 deep dive

by 나는후니 2022. 2. 16.

레벨 1 미션 레이싱카는 콘솔을 기반으로 진행됩니다. 1단계를 진행하며 작성한 단위 테스트에서는 입, 출력값을 테스트 할 필요가 없었지만 2단계 미션인 MVC리팩토링 과정에서는 컨트롤러를 테스트하는 방법에 대해 많이 고민하게 될 텐데요. 이번 포스팅에서는 입, 출력값에 대한 테스트를 추상클래스로 작성하여 상속을 통해 실제 테스트가 진행되는 코드를 깔끔하게 유지하면서 I/O를 테스트 할 수 있는 코드를 작성해보고자 합니다.

Scanner 와 System.in

먼저 테스트를 진행하기 위해서는 사용자가 원하는 입력값을 입력해야 합니다. 이번 미션의 프로덕션 코드에서 아래 예시처럼 Scanner 인스턴스를 System.in 인자로 생성 했을텐데요.

 

Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();


테스트에 입력값을 넣는 코드를 작성하기에 앞서 `Scanner` 클래스에 들어가 생성자 설명을 확인해보면 쉽게 다음 과정을 이해할 수 있습니다.

지정된 입력 스트림 (system.in)에서 스캔한 값(입력값의 byte 배열)을 생성하는 새로운 Scanner를 만들어 주는 생성자로 바이트 코드를 문자로 변환시켜 줍니다.

위 사진을 통해 어느정도 힌트를 얻을 수 있습니다. 테스트 코드가 실행될 때, System.in에 입력하고자 하는 문자의 바이트 코드 배열을 전달해주면 원하는 코드를 작성 할 수 있겠다는 생각이 듭니다.


그렇다면 어떻게 `System.in`이라는 클래스 메서드에 값을 세팅해 줄 수 있을까요?

해석해보면 "표준" 입력 스트림으로, 이미 열려 있고 사용자가 지정한 입력 소스에 따라 스트림이 정의된다고 합니다.
이 setIn 메서드의 핵심은 "표준" 입력 스트림을 재할당해 준다는 것입니다.

System 클래스의 설명에서 볼 수 있다시피, 표준 입력 스트림을 규정하는 클래스 필드에 사용자가 지정한 입력 스트림을 할당하여 원하는 입력값을 기입할 수 있습니다. 사용자는 InputStream 을 상속받는 ByteArrayInputStream 을 생성하여 입력값의 바이트 배열을 스트림에서 읽을 수 있는 버퍼에 담아 System.in에 할당함으로써 원하는 입력값을 테스팅할 수 있게 됩니다.

이 과정을 코드로 정리하면 아래와 같습니다.

public abstract class IOTest {

    // 이 메서드를 실행하면 input값의 바이트 배열을 스트림에 담아 System.in에 할당해줍니다.
    protected void systemIn(String input) {
            System.setIn(new ByteArrayInputStream(input.getBytes()));
    }
}


이 추상클래스를 상속받아 간단하게 입력값이 잘 들어가는지 테스트하는 코드를 작성해보았습니다.

public class BlogTest extends IOTest {

    @Test
    void set_in_test() {
        systemIn("원하는 입력값");
        test();
    }

    void test() {
        Scanner scanner = new Scanner(System.in);
        System.out.println(scanner.nextLine());
    }
}

작성한대로 입력값이 잘 출력되는 것을 확인할 수 있습니다. 하지만 정상 출력하는지 확인하기 위해서는 출력값을 Assertions로 비교해줘야 합니다.

System.out

System의 클래스 필드인 outin과 같습니다. 표준 스트림이 존재하고 표준 스트림을 재할당할 수 있는 메서드가 존재하죠.

표준 스트림은 System.out
표준 스트림을 PrintStream을 상속받는 클래스 중 하나로 재할당할 수 있습니다.

그렇다면 간단하게 System.out의 스트림을 출력되는 값의 바이트 코드를 받아오도록 할당해볼 수 있게 됐습니다.

IOTest를 상속받는 테스트가 실행되기 전, 출력값이 바이트 코드를 잘 print 할 수 있도록 BeforeEach 애노테이션을 달아줍니다. 그리고 해당 스트림의 바이트 코드를 String으로 바꿔 이를 비교하면서 테스트를 마칠 수 있습니다.

 

public abstract class IOTest {

    private ByteArrayOutputStream outputStreamCaptor;

    protected void systemIn(String input) {
        System.setIn(new ByteArrayInputStream(input.getBytes()));
    }

    @BeforeEach
    void setUp() {
        outputStreamCaptor = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outputStreamCaptor));
    }

    protected String getOutput() {
        // ByteArrayOutputStream의 toString은 기본 문자집합을 사용하여 버퍼의 내용을 문자열 디코딩 바이트로 변환해줍니다. 
        return outputStreamCaptor.toString();
    }
}

System.in에서 작성한 테스트 코드에서 테스트 할 수 있는 코드를 한 줄 추가해보고 테스트를 돌리면 원하는 대로 테스트가 통과하는 것을 확인할 수 있습니다.

    @Test
    void set_in_test() {
        systemIn("원하는 입력값");
        test();

        assertThat(getOutput()).contains("원하는 입력값");
    }

하지만 스트림이 ByteArrayOutputStream 으로 할당된 상태에서는 출력값을 확인할 수 없습니다 (System.out은 자동으로 flush되지 않기 때문에 그런듯 하네요). 즉, 버퍼 내에 출력값이 존재하기 때문에 System.out 표준 스트림 (콘솔에 출력되는 스트림)에 출력값이 존재하지 않는 것이죠.

이렇게 테스트를 마칠 수 도 있겠지만 원하는 내용이 정확하게 출력이 되었는지 확인하기 위해서는 System.out의 스트림을 표준 스트림으로 재할당 할 필요가 있습니다. AfterEach 애노테이션을 이용해 테스트가 끝나는 순간마다 스트림을 재할당 해줍니다. 그리고 출력된 문자열이 담긴 스트림을 디코딩한 문자열을 가져와 출력함으로써 눈으로도 원하는 내용을 확인하게 되는 것입니다.

public abstract class IOTest {

    private ByteArrayOutputStream outputStreamCaptor;
    private PrintStream standardOut; // 표준 스트림

    protected void systemIn(String input) {
        System.setIn(new ByteArrayInputStream(input.getBytes()));
    }

    @BeforeEach
    void setUp() {
        standardOut = System.out; // 표준 스트림 초기화
        outputStreamCaptor = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outputStreamCaptor));
    }

    @AfterEach
    protected void printResult() {
        System.setOut(standardOut); // 표준 스트림 할당
        System.out.println(getOutput()); // 원하는 내용이 잘 나왔는지 문자열 디코딩 바이트를 가져와 출력
    }

    protected String getOutput() {
        return outputStreamCaptor.toString();
    }
}


다시 테스트를 실행하면 아래 결과와 동일하게 "원하는 입력값"이 출력되는 것을 눈으로도 확인할 수 있습니다.

스트림을 재할당하여 출력값 확인 가능!!

이전포스팅 을 보면 미션의 테스트 코드에서 이 IOTest 추상클래스를 활용하여 테스트 코드를 작성한 내용이 게시 되어 있습니다. 두 포스팅을 참고하여 원하는 대로 입력값을 넣고 출력값을 테스트 하는 환경을 직접 구축해보면 좋을 것 같습니다!