【Kotlin/JVM】競技プログラミングのテストに！　System.in への書き込み　System.out からの読み出し

競技プログラミングの多くでは、
問題のパラメータが標準入力から与えられ、
解答を標準出力に書き込む。

単体テストライブラリーを用いて標準入出力のテストをするにはどうすればよいだろうか。
少なくとも JUnit では特にサポートはされていないようだ。

そこで、`System.in` から読み出せるテキストをコードから書き込んだり
`System.out` に書き込んだテキストをコードから読み出したりできるようにしてみた。

## 作ったもの

作成したのは次の4ファイル4クラス。

直接使うのは `WritableSystemInScope` と `ReadableSystemOutScope`。
`SystemInScope` と `SystemOutScope` は他にも使い道がありそうなので独立したクラスに切り出した。

<details><summary>SystemInScope.kt</summary><div>

```SystemInScope.kt
package test_utils

import java.io.InputStream

/**
 * 生成されてから [close] されるまでの間、
 * [System.in] を引数 in で指定されたものに切り替える。
 */
class SystemInScope(
    `in`: InputStream
) : AutoCloseable {
    private var oldIn: InputStream? = System.`in`

    init {
        System.setIn(`in`)
    }

    override fun close() {
        checkNotNull(oldIn) { "すでに close されています。" }
            .also {
                oldIn = null
                System.setIn(it)
            }
    }
}
```
</div></details>

<details><summary>SystemOutScope.kt</summary><div>

```SystemOutScope.kt
package test_utils

import java.io.PrintStream

/**
 * 生成されてから [close] されるまでの間、
 * [System.out] を引数 out で指定されたものに切り替える。
 */
class SystemOutScope(
    out: PrintStream
) : AutoCloseable {
    private var oldOut: PrintStream? = System.out

    init {
        System.setOut(out)
    }

    override fun close() {
        checkNotNull(oldOut) { "すでに close されています。" }
            .also {
                oldOut = null
                System.setOut(it)
            }
    }
}
```
</div></details>

<details><summary> WritableSystemInScope.kt</summary><div>

```WritableSystemInScope.kt
package test_utils

import java.io.BufferedWriter
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.io.Writer

/**
 * 生成されてから [close] されるまでの間
 * [System. in] を切り替えて、
 * プロパティ [writer] に書き込まれたものを [System. in] から読み出せるようにする。
 */
class WritableSystemInScope : AutoCloseable {
    private val pipedOutStream = PipedOutputStream()
    private val `in` = PipedInputStream(pipedOutStream)
    private val systemInScope = SystemInScope(`in`)

    /**
     * [System. in] から読み出せるものを書き込むための [Writer]。
     *
     * [System. in] を終端させるために [close] してもよい。
     */
    val writer: BufferedWriter = pipedOutStream.bufferedWriter()

    override fun close() {
        writer.close()
        systemInScope.close()
        `in`.close()
    }
}
```
</div></details>

<details><summary> ReadableSystemOutScope.kt </summary><div>

```ReadableSystemOutScope.kt
package test_utils

import java.io.*

/**
 * 生成されてから [close] されるまでの間
 * [System.out] を切り替えて、
 * [System.out] に書き込まれたものをプロパティ [reader] から読み出せるようにする。
 */
class ReadableSystemOutScope : AutoCloseable {
    private val pipedInStream = PipedInputStream()
    private val out = PrintStream(PipedOutputStream(pipedInStream))
    private val systemOutScope = SystemOutScope(out)

    /** [System.out] に書き込まれたものを読み出すための [Reader]。 */
    val reader: BufferedReader = pipedInStream.bufferedReader()

    override fun close() {
        reader.close()
        systemOutScope.close()
        out.close()
    }
}
```
</div></details>

## 使い方

`WritableSystemInScope` と `ReadableSystemOutScope` のインスタンスは
生成されてから `close` されるまでの間、
それぞれ `System.in` と `System.out` を差し替える。
その間、それぞれが持つプロパティからテキストの書き込み・読み出しができる。

基本的な使い方は次のようになる。

<details><summary>基本的な使い方</summary><div>

```Kotlin
package test_utils

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test

class SystemIoScopeUseTest {
    @Test
    fun test_WritableSystemInScope() {
        WritableSystemInScope().use {
            val writer = it.writer

            // writer に書き込む。
            writer.appendln("I love Kotlin!")
            writer.flush()

            assertEquals(
                "I love Kotlin!",
                // System.in から読み込む。
                readLine()
            )

            // writer を close すると System.in が終端する。
            writer.close()
            assertNull(
                readLine()
            )
        }
    }

    @Test
    fun test_ReadableSystemOutScope() {
        ReadableSystemOutScope().use {
            val reader = it.reader

            // System.out に書き込む。
            println("I love Kotlin!")

            assertEquals(
                "I love Kotlin!",
                // reader から読み込む。
                reader.readLine()
            )
        }
    }
}
```
</div></details>

多くのテストケースで使う場合は、`setUp` で生成して `tearDown` で `close` するとよい。

<details><summary>`setUp` で生成して `tearDown` で `close` するやり方</summary><div>

```Kotlin
package test_utils

import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.BufferedReader
import java.io.BufferedWriter

class SystemIoScopeSetUpTearDownTest {
    private lateinit var writableSystemInScope: WritableSystemInScope
    private lateinit var readableSystemOutScope: ReadableSystemOutScope

    private val systemInWriter: BufferedWriter
        get() = writableSystemInScope.writer
    private val systemOutReader: BufferedReader
        get() = readableSystemOutScope.reader

    @BeforeEach
    fun setUp() {
        writableSystemInScope = WritableSystemInScope()
        readableSystemOutScope = ReadableSystemOutScope()
    }

    @AfterEach
    fun tearDown() {
        readableSystemOutScope.close()
        writableSystemInScope.close()
    }

    @Test
    fun test_WritableSystemInScope() {
        // writer に書き込む。
        systemInWriter.appendln("I love Kotlin!")
        systemInWriter.flush()

        assertEquals(
            "I love Kotlin!",
            // System.in から読み込む。
            readLine()
        )

        // writer を close すると System.in が終端する。
        systemInWriter.close()
        assertNull(
            readLine()
        )
    }

    @Test
    fun test_ReadableSystemOutScope() {
        // System.out に書き込む。
        println("I love Kotlin!")

        assertEquals(
            "I love Kotlin!",
            // reader から読み込む。
            systemOutReader.readLine()
        )
    }
}
```
</div></details>

／以上