목차
들어가기 전에
간단한 토이 프로젝트를 진행하던 중에 콘솔을 통해 사용자로부터 값을 입력받아서 처리하는 로직을 구현하게 되었다.
이 로직을 테스트하기 위해 단순히 IDE로 프로젝트를 실행해서 돌려볼 수도 있었지만 입력받아서 처리하는 로직만 테스트를 하고 싶었기 때문에 단위 테스트로 구현하고자 했다.
그런데 그냥 평범하게 콘솔 입력을 하면 되겠지라는 생각과는 달리 단위 테스트에서는 콘솔 입력을 사용할 수 없었다. 하지만 단위 테스트에서도 콘솔로 입력하지 않을 뿐이지 표준 입력을 사용하는 로직을 테스트하는 방법을 찾을 수 있어서 정리해두려고 한다.
테스트를 하려는 기능
fun readConfig() = System.`in`.bufferedReader().run {
val host = input("Host: ")
val port = input("Port: ")
val uri = input("URI: ")
Config(host, port, uri).apply { save("./config.json") }
}
private fun BufferedReader.input(prompt: String): String {
print(prompt)
return readLine()
}
private fun Config.save(path: String) {
val json = Json { prettyPrint = true }
val file = File(path)
file.writeText(json.encodeToString(serializer(), this))
}
이 메소드는 사용자에게 Host, Port, URI를 입력받아서 Config 객체를 반환하고, 동시에 config.json
파일에 해당 정보를 JSON 형태로 저장하는 메소드이다.
이렇게 생성된 config.json
에 사용자가 입력한 정보가 제대로 저장되어 있는지 확인하고 싶었다.
테스트를 진행해 보자
테스트를 위한 테스트 코드를 작성한다.
@Test
fun `콘솔 설정 및 저장 테스트`() {
val config = readConfig()
val fileConfig = jsonFileToConfig("./config.json")
assertThat(config).isEqualTo(fileConfig)
}
여기서 jsonFileToConfig()
는 파일에 있는 JSON을 읽어서 Config 객체로 변환해 주는 메소드이다. 이 메소드를 통해 생성된 Config 객체와 사용자 입력을 통해 생성된 Config 객체를 비교하여 일치한다면 사용자 입력이 정상적으로 json 파일에 저장됐다고 확인할 수 있을 것이다.
이제 테스트 코드를 실행해 본다.
테스트 코드를 실행하기만 했는데 입력을 받기는커녕 실행하자마자 NPE가 발생하고 있다.
원인은 무엇일까?
JUnit을 포함한 단위 테스트 도구들은 기본적으로 다른 구성 요소들과의 상호작용 없이 예상대로 작동하는지 확인할 수 있도록 설계되어 있다. 이러한 상호작용에는 사용자로부터 입력을 받는 것 또한 포함된다.
이로 인해 JUnit 테스트에는 입력을 읽을 수 있는 콘솔이 존재하지 않아 예외가 발생하게 된다.
그래도 방법이 있다
콘솔 입력은 표준 입력인 System.in
에 정의되어 있는 InputStream을 사용한다. 자바/코틀린에서는 System.setIn()
메소드를 통해 표준 입력 스트림을 재정의할 수 있다.
즉, ByteArrayInputStream 등의 InputStream 구현체를 표준 입력 스트림으로 재정의한다면 사용자 입력을 받는 로직도 JUnit을 통한 단위 테스트가 가능하다!
이 글에서는 ByteArrayInputStream을 사용하는 방법을 다룰 것이다.
구체적인 방법은 다음과 같다.
우선 다음과 같이 인자로 받은 문자열을 ByteArrayInputStream으로 변환하여 표준 입력으로 재정의하는 메소드를 작성해 준다.
Kotlin
private fun setInput(input: String) {
System.setIn(input.byteInputStream())
}
Java
private void setInput(String input) {
System.setIn(new ByteArrayInputStream(input.getBytes()));
}
그 후 위의 테스트 코드에서 표준 입력을 사용하는 메소드를 호출하기 전에 setInput()
을 호출해서 표준 입력을 재정의해준다.
겸사겸사 given을 고정했으니 then에서 검증할 부분도 조금 추가해줬다.
fun `콘솔 설정 및 저장 테스트`() {
setInput("""
http://localhost
8080
/hello
""".trimIndent())
val config = readConfig()
val fileConfig = jsonFileToConfig("./config.json")
assertThat(config.host).isEqualTo("http://localhost")
assertThat(config.port).isEqualTo(8080)
assertThat(config.uri).isEqualTo("/hello")
assertThat(config).isEqualTo(fileConfig)
}
그 후 테스트 코드를 실행해 보면 테스트에 성공하는 것을 확인할 수 있다.
생성된 config.json
파일을 확인했을 때도 의도한 내용대로 파일이 생성된 것을 확인할 수 있다.
표준 입력 재정의 시 주의할 점
하나의 테스트 코드만 실행할 때는 표준 입력을 재정의하더라도 문제가 되지 않지만 그래들의 test
명령이나 클래스의 테스트 코드를 한 번에 실행할 때는 문제가 될 수 있다. 재정의된 표준 입력이 다른 테스트 코드에 영향을 줄 수 있기 때문이다.
이를 방지하기 위해 테스트 클래스 내에 @BeforeEach
또는 @AfterEach
로 각 테스트 코드 실행 전 또는 실행 후에 표준 입력을 초기화하는 메소드를 작성해 주는 것이 좋다.
Kotlin
private val initialInput = System.`in`
@BeforeEach
fun resetInput() {
System.setIn(initialInput)
}
Java
private InputStream initialInput = System.in;
@BeforeEach
void resetInput() {
System.setIn(initialInput);
}
마치며
글에서는 표준 입력에 대한 내용만을 이야기했지만 표준 출력을 사용하는 로직도 비슷한 방법으로 출력 스트림을 재정의하여 단위 테스트가 가능하다고 한다.
단순히 JUnit 단위 테스트에서 콘솔 입력을 사용할 수 없어서 시작했던 문제였는데 글을 쓰는 과정에서 단위 테스트에 대해 다시 돌아볼 수 있는 계기가 되었다.
이 글을 작성하는 계기가 된 상황에선 "어쨌든 assertThat()
으로 원하는 대로 동작하는지 확인하니까 입력정도는 직접 넣어도 괜찮지 않을까" 하는 생각으로 진행을 했다. 하지만 단위 테스트에 대해 잘못 이해해서 이런 생각을 가지게 되었다는 것도 알게 되었다.
단순히 단위 테스트가 아니더라도 비슷하게 잘못 이해하고 있는 개념이 있진 않은지 돌아볼 계기가 될 것 같다.
참조링크
'Programming Language > Kotlin & Java' 카테고리의 다른 글
[Java] UnmodifiableList는 진짜 불변 리스트가 아니다 (0) | 2023.08.15 |
---|---|
[Java] Java 14부터 추가된 Record 타입과 Kotlin의 Data Class 비교 (0) | 2023.06.14 |
[Kotlin] 확장 함수와 람다를 사용해서 중복 코드를 제거해보자! (0) | 2023.05.08 |
[Java/Kotlin] 필드(Field)와 프로퍼티(Property)는 무슨 차이가 있을까? (0) | 2023.05.02 |
[Kotlin] Java와 Kotlin, 그리고 Lombok (0) | 2023.04.21 |
댓글