기록의 정석

개발/기타

System.in과 System.out에 대한 테스트

sakjung 2021. 2. 13. 14:46

우테코 자동차 경주 콘솔게임을 구현하면서 특히 단위 테스트 작성 연습에 집중했다. 이제 어느정도 단위 테스트에 대해 자신감이 차올랐을 때 문득 궁금한 점이 생겼다.

 

System.in과 System.out 관련 로직들은 어떻게 테스트하지?

 

콘솔로 부터 사용자의 입력을 받는 것 (e.g. 자동차 이름, 경기 진행 횟수)이나 콘솔에 게임에 관련해서 출력해주는 것 (e.g. 우승자 발표를 정해진 형식에 맞게 출력)은 콘솔이 있어야만 테스트 가능한 것 아닌가? 근데 테스트 할때 어떻게 콘솔을 이용해먹지? 이런 생각을 하며 자린이인 나는 도저히 어떤 식으로 해야할지 감이 안왔다. 그래서 이를 간단하게 테스트 할 방법이 없을까에 대해 알아보기 위해서 그때부터 구글링을 시작했다.

 

역시나 stackoverflow에서 멋진 형님들이 이미 오래전에 이와 관련해서 많은 얘기를 나누고 계셨었다. 그래서 관련 글들을 읽고 적용해본 점을 기록해보고자 한다.

 

System.in

콘솔로 부터 사용자의 input을 받을 때에는 Scanner가 필요하다. 그리고 Scanner를 생성할 때 안에 다음과 같이 인자로 System.in을 넣어주게 된다. 다음과 같이 생성된 Scanner는 다양한 메소드를 통해 콘솔로 부터 사용자가 입력하는 값을 원하는대로 받아올 수 있다.

 

Scanner scanner = new Scanner(System.in);

 

이 때, System.in은 콘솔창에서 사용자가 입력하는 input 값을 InputStream으로 담는 역할을 한다. 그러면 Scanner는 이 InputStream을 확인하여 사용자의 input을 읽어들이는(Scan) 것이다. 이 원리를 이해하면 이제 테스트 코드 작성은 크게 어렵지 않다.

 

콘솔창에서 일어나는 것과 같은 상황을 재현 (Mock)하기 위해서 나는 다음과 같은 과정으로 진행했다.

 

1. System.in 대신 Scanner에 넣을 InputStream (테스트용 mock InputStream)을 직접 만든다.

2. 이 InputStream을 실제 콘솔값을 받아주는 System.in으로 세팅해준다. 이제 System.in 안에는 우리의 테스트 데이터가 이미 들어가있다.

3. System.in을 가지고 Scanner를 만들어준다.  

 

실제 코드를 예시로 과정을 한번 살펴보자

우선 mock InputStream을 만들어야 한다. 나의 경우에는 이런 식으로 메서드로 만들어 뒀다.

 

public static InputStream generateUserInput(String input) {
	return new ByteArrayInputStream(input.getBytes());
}

 

String 상태로 내가 넣어준 테스트용 input을 바이트 코드로 바꾸어 준다. 그 후 바이트 코드 상태의 input을 ByteArrayInputStream의 인자로 넘겨주어서 InputStream을 생성했다. 이렇게 하면 콘솔로 부터 입력받은 사용자 input을 InputStream으로 저장하는 과정을 흉내낸것이다.

 

다음은 입력된 자동차 이름이 1 미만 5 초과일 시 에러를 제대로 반환하는지에 대한 테스트를 해보는 코드이다.

 

generateUserInput메서드로 사용자 Input을 만들고 이를 System.setIn() 의 인자로 넘겨주면 된다. 그러면 사용자 input을 실제 콘솔의 입력을 담는 System.in의 InputStream으로 세팅해줄 수가 있다. 이제 System.in은 우리가 원하는 Input값을 품고있다.

 

그 다음부터는 Scanner를 만들고 무난하게 테스트를 진행시켜주면 된다. 이제 사진 속 scanner는 진짜 콘솔에서 사용자 input을 받은 Scanner와 같은 녀석이 되버린 것이다. 뭔가 복제인간을 생성하는 느낌이기도 하다.

 

그런데 만약 input이 여러개로 들어오는 상황을 재현하고 싶다면 어떻게 하면 될까??

 

나의 경우에는 컨트롤러에 관한 테스트를 진행할 때 이런 필요에 대해서 고민하게 되었다. 왜냐하면 컨트롤러의 게임진행 메서드에서는 자동차이름에 대한 input과 라운드 횟수에 대한 input, 총 2번에 걸쳐서 input을 받아야 했기 때문이다. 즉, 게임진행 메서드가 제대로 동작하는지 테스트하기 위해서는 2번에 걸쳐서 input을 받는 상황을 재현해야 했다.

 

이는 다음과 같은 방법으로 테스트 할 수 있었다.

 

우선 provideNameAndNumberOfRoundsInput 메서드를 통해 MethodSource 테스트 케이스를 제공해준다. 각 Argument의 첫 번째 인자는 자동차 이름 (첫번째 사용자 input)이고 두 번째 인자는 라운드 횟수 (두번째 사용자 input)이다.

 

createInputStream 메서드는 위에서 제공해주는 인자들을 받아서 하나의 InputStream으로 연결시켜주는 역할을 한다. 사실 위의 경우처럼 2개의 stream만을 이어붙이는 경우라면 Collections.enumeration()을 사용하지 않고 바로 SequenceInputStream(inputStream1, inputstream2)와 같이 연결시켜 줄 수도 있다. Collections.enumeration()은 3개 이상의 InputStream을 연결 시켜줘야하는 경우에 사용하면 된다.

 

그 다음은 이를 다음과 같이 사용하여 테스트를 진행하면 된다. 참고로 isControllerRunningSuccessfully는 컨트롤러가 제대로 동작하는지 안하는지에 대해 boolean을 반환하는 응답형 메서드이다. 아무튼 중요한 점은 RacingGameController는 scanner를 받아서 n개의 실제 사용자 인풋을 콘솔을 통해 차례 차례 받아먹는 것 처럼 동작하면서 테스트를 수행할 것이다.

 

 

Sytem.out

System.out관련 테스트의 경우에는 System.in의 경우와 반대로 동작한다고 생각하면 쉽다. System.in은 InputStream이므로 테스트 메서드가 소비하는 대상이다. 즉, 테스를 위해서 이미 채워진 System.in을 야금야금 읽어들이며 테스트를 진행한다. 하지만 System.out은 PrintStream이므로 테스트 메서드가 채워나가야 하는 대상이다. 그러므로 처음에 비어있는 상태로 시작하고 테스트를 진행하면서 PrintStream이 출력한 내용대로 하나하나 채워진다.

 

이번 경우에는 System.out을 직접 사용하지 않고 System.out의 대타로 매 테스트 마다 새로운 PrintStream을 생성해서 System.out 대신 사용해주는 방법으로 테스트를 진행해보자. 이를 위한 기본 세팅은 다음과 같다.

 

 

ByteArrayOutputStream은 메서드가 출력하는 출력인자들을 바이트 배열로 저장한다. 이를 활용하여 비어있는 PrintStream을 만들어주고 매 테스트가 실행되지 전에 이를 System.out 대신 세팅해준다. 이제 테스트가 출력하는 모든 문구들은 output에 바이트 형태로 저장될 것이다. 그리고 매 메서드가 끝난 후에는 System.out으로 세팅을 돌려주자. 안 돌려놓으면 나중에 다른 곳에서 프린트 관련 로직을 실행할 때 문제가 생길 수도 있다. 그리고 output.reset()을 통해 실행된 메서드로부터 쌓인 출력값들을 비워주고 다음 메서드를 위해 사용될 수 있게 만들어준다.

 

다음은 매 라운드 마다 출력되는 리더보드 형식이 제대로 되어 있는지 확인하는 테스트 코드이다.

 

OutputView.printLeaderBoard 메서드가 실행되면 output에 출력된 리더보드가 저장될 것이다. 그러면 우리는 이를 우리가 예상했던 expectedLeaderBoardFormat과 일치하는지 확인하면 OutputView.printLeaderBoard가 제대로 출력문을 작성하는지 확인할 수 있다.

 

 

 

 

 

 

- Reference

System.in

https://stackoverflow.com/questions/31635698/junit-testing-for-user-input-using-scanner

https://stackoverflow.com/questions/16066671/how-can-i-unit-test-user-input-in-java

https://stackoverflow.com/questions/1647907/junit-how-to-simulate-system-in-testing

https://stackoverflow.com/questions/7099279/adding-characters-to-beginning-and-end-of-inputstream-in-java

 

System.out

https://stackoverflow.com/questions/1119385/junit-test-for-system-out-println/1119559#1119559

http://wiki.gurubee.net/display/SWDEV/ByteArrayInputStream%2C+ByteArrayOutputStream

'개발 > 기타' 카테고리의 다른 글

[Python/파이썬] 진법 변환  (0) 2020.12.29