=================
== The Archive ==
=================

Kotlin으로 유저 Input 테스트 코드 작성하기

|

Introduction

문제

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package essentials.exceptions.handleexceptions

fun main() {
    while (true) {
        // Wrap below function call with try-catching block,
        // and handle possible exceptions.
        handleInput()
    }
}

fun handleInput() {
    print("Enter the first number: ")
    val num1 = readln().toInt()
    print("Enter an operator (+, -, *, /): ")
    val operator = readln()
    print("Enter the second number: ")
    val num2 = readln().toInt()

    val result = when (operator) {
        "+" -> num1 + num2
        "-" -> num1 - num2
        "*" -> num1 * num2
        "/" -> num1 / num2
        else -> throw IllegalOperatorException(operator)
    }

    println("Result: $result")
}

class IllegalOperatorException(val operator: String) :
    Exception("Unknown operator: $operator")

분석

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@SinceKotlin("1.6")
public actual fun readln(): String = readlnOrNull() ?: throw ReadAfterEOFException("EOF has already been reached")

@SinceKotlin("1.6")
public actual fun readlnOrNull(): String? = readLine()

public fun readLine(): String? = LineReader.readLine(System.`in`, Charset.defaultCharset())

internal object LineReader {
    private const val BUFFER_SIZE: Int = 32
    private lateinit var decoder: CharsetDecoder
    private var directEOL = false
    private val bytes = ByteArray(BUFFER_SIZE)
    private val chars = CharArray(BUFFER_SIZE)
    private val byteBuf: ByteBuffer = ByteBuffer.wrap(bytes)
    private val charBuf: CharBuffer = CharBuffer.wrap(chars)
    private val sb = StringBuilder()

    /**
     * Reads line from the specified [inputStream] with the given [charset].
     * The general design:
     * * This function contains only fast path code and all it state is kept in local variables as much as possible.
     * * All the slow-path code is moved to separate functions and the call-sequence bytecode is minimized for it.
     */
    @Synchronized
    fun readLine(inputStream: InputStream, charset: Charset): String? { // charset == null -> use default
        if (!::decoder.isInitialized || decoder.charset() != charset) updateCharset(charset)
        var nBytes = 0
        var nChars = 0
        while (true) {
            val readByte = inputStream.read()
            if (readByte == -1) {
                // The result is null only if there was absolutely nothing read
                if (sb.isEmpty() && nBytes == 0 && nChars == 0) {
                    return null
                } else {
                    nChars = decodeEndOfInput(nBytes, nChars) // throws exception if partial char
                    break
                }
            } else {
                bytes[nBytes++] = readByte.toByte()
            }
            // With "directEOL" encoding bytes are batched before being decoded all at once
            if (readByte == '\n'.code || nBytes == BUFFER_SIZE || !directEOL) {
                // Decode the bytes that were read
                byteBuf.limit(nBytes) // byteBuf position is always zero
                charBuf.position(nChars) // charBuf limit is always BUFFER_SIZE
                nChars = decode(false)
                // Break when we have decoded end of line
                if (nChars > 0 && chars[nChars - 1] == '\n') {
                    byteBuf.position(0) // reset position for next use
                    break
                }
                // otherwise we're going to read more bytes, so compact byteBuf
                nBytes = compactBytes()
            }
        }
        // Trim the end of line
        if (nChars > 0 && chars[nChars - 1] == '\n') {
            nChars--
            if (nChars > 0 && chars[nChars - 1] == '\r') nChars--
        }
        // Fast path for short lines (don't use StringBuilder)
        if (sb.isEmpty()) return String(chars, 0, nChars)
        // Copy the rest of chars to StringBuilder
        sb.append(chars, 0, nChars)
        // Build the result
        val result = sb.toString()
        if (sb.length > BUFFER_SIZE) trimStringBuilder()
        sb.setLength(0)
        return result
    }

    // The result is the number of chars in charBuf
    private fun decode(endOfInput: Boolean): Int {
        while (true) {
            val coderResult: CoderResult = decoder.decode(byteBuf, charBuf, endOfInput)
            if (coderResult.isError) {
                resetAll() // so that next call to readLine starts from clean state
                coderResult.throwException()
            }
            val nChars = charBuf.position()
            if (!coderResult.isOverflow) return nChars // has room in buffer -- everything possible was decoded
            // overflow (charBuf is full) -- offload everything from charBuf but last char into sb
            sb.append(chars, 0, nChars - 1)
            charBuf.position(0)
            charBuf.limit(BUFFER_SIZE)
            charBuf.put(chars[nChars - 1]) // retain last char
        }
    }

    // Slow path -- only on long lines (extra call to decode will be performed)
    private fun compactBytes(): Int = with(byteBuf) {
        compact()
        return position().also { position(0) }
    }

    // Slow path -- only on end of input
    private fun decodeEndOfInput(nBytes: Int, nChars: Int): Int {
        byteBuf.limit(nBytes) // byteBuf position is always zero
        charBuf.position(nChars) // charBuf limit is always BUFFER_SIZE
        return decode(true).also { // throws exception if partial char
            // reset decoder and byteBuf for next use
            decoder.reset()
            byteBuf.position(0)
        }
    }

    // Slow path -- only on charset change
    private fun updateCharset(charset: Charset) {
        decoder = charset.newDecoder()
        // try decoding ASCII line separator to see if this charset (like UTF-8) encodes it directly
        byteBuf.clear()
        charBuf.clear()
        byteBuf.put('\n'.code.toByte())
        byteBuf.flip()
        decoder.decode(byteBuf, charBuf, false)
        directEOL = charBuf.position() == 1 && charBuf.get(0) == '\n'
        resetAll()
    }

    // Slow path -- only on exception in decoder and on charset change
    private fun resetAll() {
        decoder.reset()
        byteBuf.position(0)
        sb.setLength(0)
    }

    // Slow path -- only on long lines
    private fun trimStringBuilder() {
        sb.setLength(BUFFER_SIZE)
        sb.trimToSize()
    }
}

해결

1
2
3
4
5
6
7
8
9
private fun <T> mockUserInput(
    vararg input: String,
    block: () -> T,
): T {
    val testIn = ByteArrayInputStream(input.joinToString("\n").toByteArray())
    System.setIn(testIn)

    return block.invoke()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class HandleExceptionsTest {
    private val sut = HandleExceptions()

    private fun <T> mockUserInput(
        vararg input: String,
        block: () -> T,
    ): T {
        val testIn = ByteArrayInputStream(input.joinToString("\n").toByteArray())
        System.setIn(testIn)

        return block.invoke()
    }

    private fun String.withPrintResult(): String {
        println("\nresult: `$this`")
        return this
    }

    @Test
    fun wrongNumberTest() {
        val result =
            mockUserInput("NaN", "/", "NaN") {
                assertDoesNotThrow {
                    sut.handleInputWithoutExceptions()
                }
            }.withPrintResult()

        assertThat(result).startsWith("Invalid input: ")
    }

    @Test
    fun divisionByZeroTest() {
        val result =
            mockUserInput("0", "/", "0") {
                assertDoesNotThrow {
                    sut.handleInputWithoutExceptions()
                }
            }.withPrintResult()

        assertEquals("Division by zero", result)
    }

    @Test
    fun illegalOperatorExceptionTest() {
        val result =
            mockUserInput("0", "//", "0") {
                assertDoesNotThrow {
                    sut.handleInputWithoutExceptions()
                }
            }.withPrintResult()

        assertThat(result).startsWith("Illegal operator: ")
    }

    @ParameterizedTest
    @CsvSource(
        "1,+,1,2",
        "1,-,2,-1",
        "2,*,3,6",
        "4,/,2,2",
        "4,/,3,1",
    )
    fun withoutAnyExceptionsTest(
        num1: String,
        operator: String,
        num2: String,
        expected: String,
    ) {
        val result =
            mockUserInput(num1, operator, num2) {
                assertDoesNotThrow {
                    sut.handleInputWithoutExceptions()
                }
            }.withPrintResult()

        assertEquals("Result: $expected", result)
    }
}

References

Categories:

Tags: