레벨 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
에 입력하고자 하는 문자의 바이트 코드 배열을 전달해주면 원하는 코드를 작성 할 수 있겠다는 생각이 듭니다.
그렇다면 어떻게 `System.in`이라는 클래스 메서드에 값을 세팅해 줄 수 있을까요?
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
의 클래스 필드인 out
도 in
과 같습니다. 표준 스트림이 존재하고 표준 스트림을 재할당할 수 있는 메서드가 존재하죠.
그렇다면 간단하게 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
추상클래스를 활용하여 테스트 코드를 작성한 내용이 게시 되어 있습니다. 두 포스팅을 참고하여 원하는 대로 입력값을 넣고 출력값을 테스트 하는 환경을 직접 구축해보면 좋을 것 같습니다!
'우아한테크코스 4기 > 레벨1' 카테고리의 다른 글
[Java] Java 예외처리와 StackOverFlow (0) | 2022.02.21 |
---|---|
[Java] Enum타입 사용하기 (1) | 2022.02.19 |
[Java] Mockito를 사용하여 TDD 적용해보기 (0) | 2022.02.17 |
[Java] 대역으로 Junit 테스트 코드 작성하기 (0) | 2022.02.15 |
[Java] 인터페이스로 분리하여 객체 테스트하기 (2) | 2022.02.13 |