๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/๊ธฐํƒ€

Unit Testing - Test double

interfacer_han 2024. 6. 30. 08:33

#1 Test double

fun main() {
    // Dependency (Test double) ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val testDouble = TestDouble()

    // Dependent ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val dependent = Dependent(testDouble)

    // Unit test ์ง„ํ–‰
    if (dependent.doMathOperation(10) == 55) {
        println("ํ…Œ์ŠคํŠธ ์„ฑ๊ณต")
    } else {
        println("ํ…Œ์ŠคํŠธ ์‹คํŒจ")
    }
}

class Dependent(private val mathCalculator: OriginalDependency) {
    fun doMathOperation(n: Int): Int {
        return mathCalculator.sumFromOneToN(n)
    }
}

open class OriginalDependency {
    open fun sumFromOneToN(n: Int): Int {
        var sum = 0
        for (i: Int in 1..n) {
            sum += i
        }
        return sum
    }
}

class TestDouble : OriginalDependency() { // Dependency class for testing.
    override fun sumFromOneToN(n: Int): Int {
        return 55
    }
}

์–ด๋–ค ํ”„๋กœ์ ํŠธ๋“  Dependent - Dependency ๊ด€๊ณ„๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์„๊ฒƒ์ด๋‹ค. ์ƒ์„ฑ์ž ์ฃผ์ž…์œผ๋กœ ์˜์กด์„ฑ ์ฃผ์ž…์ด ๊ตฌํ˜„๋œ ์œ„ ์ฝ”๋“œ์˜ Dependent ํด๋ž˜์Šค์ฒ˜๋Ÿผ ๋ง์ด๋‹ค. ์ด ๊ตฌ์กฐ๋ฅผ Unit Testํ•˜๋ ค๋ฉด Dependent๊ฐ€ ์š”๊ตฌํ•˜๋Š” ์›๋ž˜์˜ Dependency๋ฅผ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ์˜๋„๋˜๋Š” ๊ฒฐ๊ณผ์™€ ํ…Œ์ŠคํŠธ์šฉ Dependency๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋น„๊ตํ•˜์—ฌ, Denpendent ๊ฒ€์ฆํ•œ๋‹ค. ์ด๋•Œ ์ด ๋ชจ์˜ ์ข…์†์„ฑ์„ ์ œ๊ณตํ•˜๋Š” ํ…Œ์ŠคํŠธ์šฉ Dependency๋ฅผ Test doubles์ด๋ผ ๋ถ€๋ฅธ๋‹ค (์ฐธ๊ณ ๋กœ, ์œ„ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•œ Test double์˜ ์ข…๋ฅ˜๋Š” #3-3์— ์žˆ๋Š” Stub์ด๋‹ค).
 

#2 Test doubles์˜ ์ข…๋ฅ˜

#2-1 ๊ฐœ์š”

class Math(private val mathCalculator: Calculation) {
    fun doMathOperation(radius: Double): Double {
        return mathCalculator.circleArea(5.0)
    }
}

open class Calculation {

    companion object {
        const val PI = 3.141592
    }

    open fun circleArea(radius: Double): Double {
        println("๋กœ๊ทธ: circleArea(${radius}) ์ˆ˜ํ–‰")
        return piMultiply(square(radius))
    }

    open fun square(operand: Double): Double {
        println("๋กœ๊ทธ: ${operand}์˜ ์ œ๊ณฑ ์—ฐ์‚ฐ ์ˆ˜ํ–‰")
        return operand * operand
    }

    open fun piMultiply(operand: Double): Double {
        println("๋กœ๊ทธ: ${operand} ๊ณฑํ•˜๊ธฐ ํŒŒ์ด ์—ฐ์‚ฐ ์ˆ˜ํ–‰")
        return operand * PI
    }
}

ํ”„๋กœ์ ํŠธ์— ์ด๋Ÿฌํ•œ ๊ตฌ์กฐ๊ฐ€ ์žˆ๋‹ค๊ณ  ์น˜์ž. ๋‚ด๊ฐ€ Unit testํ•  ํด๋ž˜์Šค๋Š” Dependent์ธ Math ํด๋ž˜์Šค๋‹ค. ๋ฌผ๋ก  ์œ„์˜ ์˜ˆ์‹œ์—์„œ๋Š” ๊ฒ€์ฆํ•  ๊ฒƒ๋„ ์—†๋Š” ์ดˆ๋ผํ•œ ํด๋ž˜์Šค์ง€๋งŒ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋งค์šฐ ๋ฐฉ๋Œ€ํ•œ ํฌ๊ธฐ์˜ ํด๋ž˜์Šค์ผ ๊ฒƒ์ด๋‹ค. #2-2 ~ #2-4์—์„œ ์ด ํด๋ž˜์Šค์— ๋Œ€ํ•œ Test Double์„ ์ข…๋ฅ˜๋ณ„๋กœ ๋งŒ๋“ค์–ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด ๋ณด๊ฒ ๋‹ค.
 

#2-2 Fake (์‹ค์ œ ๊ตฌํ˜„์˜ ๋‹จ์ˆœํ™”)

// Fake ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•œ Unit test
fun main() {
    // Dependency (Test double) ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val testDouble = FakeCalculation()

    // Dependent ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val dependent = Math(testDouble)

    // (radius๊ฐ’์œผ๋กœ 5.0์ด ๋“ค์–ด๊ฐ„๋‹ค๊ณ  ๊ฐ€์ • ํ›„) ๊ฐ’์ด ๊ทธ๋Ÿญ์ €๋Ÿญํ•œ ๊ทผ์‚ฌ๊ฐ’์œผ๋กœ๋ผ๋„ ์—๋Ÿฌ์—†์ด ์ถœ๋ ฅ๋˜๋Š”์ง€๋งŒ ํ™•์ธ
    val result = dependent.doMathOperation(5.0)
    println("Fake ์ถœ๋ ฅ ๊ฒฐ๊ณผ ํ™•์ธ: ${result}")
}

class FakeCalculation : Calculation() {
    override fun circleArea(radius: Double): Double {
        return 5 * 5 * 3.0
    }
}

์ฃผ๋กœ Dependency์— ๋Œ€ํ•œ ๊ฒ€์ฆ์ด ๋”ฑํžˆ ํ•„์š”์—†์„ ๋•Œ ์“ด๋‹ค. ํ•„์š”ํ•œ ๊ฒƒ์€ ๊ทธ๋Ÿญ์ €๋Ÿญ ๋‚˜์˜ค๋Š”๊ธฐ๋งŒ ํ•˜๋ฉด ๋˜๋Š” ์ถœ๋ ฅ๊ฐ’์ด๋‹ค. ์›๋ž˜ ํด๋ž˜์Šค์ธ Calculation์—์„  circleArea()ํ•จ์ˆ˜๊ฐ€ square() ๋ฐ piMultiply() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐ ์ด ์ ˆ์ฐจ๋„ ์ง€ํ‚ค์ง€ ์•Š๋Š”๋‹ค. ์ ˆ์ฐจ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์‹ฌ์ง€์–ด ์ถœ๋ ฅ๊ฐ’์กฐ์ฐจ ์ •ํ™•ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. Fake Denpendency๋กœ๋ถ€ํ„ฐ ์ ๋‹นํžˆ ๊ทผ์‚ฌํ•œ ์ถœ๋ ฅ๊ฐ’์ด ๋‚˜์˜จ๋‹ค๋Š” ๊ฒƒ๋งŒ ํ™•์ธํ•˜๊ณ , Dependent์˜ ๊ฒ€์ฆ์— ์ง‘์ค‘ํ•˜๋ฉด ๋œ๋‹ค.
 
์˜ˆ๋ฅผ ๋“ค์–ด, ์–ด๋–ค Repository๋ฅผ ์ข…์†์„ฑ์œผ๋กœ ๊ฐ€์ง€๋Š” ViewModel์ด ์žˆ๋‹ค๊ณ  ํ•ด๋ณด์ž. ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๋ฅผ ์ง  ํ”„๋กœ๊ทธ๋ž˜๋จธ๋Š” ViewModel์„ ์œ„์ฃผ๋กœ Unit testํ•˜๋ ค๊ณ  ํ•œ๋‹ค. ์ด ๋•Œ, Repository๋ฅผ Fakeํ˜•์‹์˜ Test double๋กœ ๋‘˜๋งŒํ•˜๋‹ค. Fake Repository์—์„œ๋Š” ์›๋ž˜ Repository์—์„œ์™€๋Š” ๋‹ฌ๋ฆฌ Room์ด๋‚˜ Retrofit์„ ๊ฑฐ์นœ๋‹ค๋Š” ์ ˆ์ฐจ๋„ ๋”ฐ๋ฅด์ง€ ์•Š๊ณ , ํ•ด๋‹น ํด๋ž˜์Šค์˜ ๋ฉ”์†Œ๋“œ๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ์ฒด๋„ ๋Œ€์ถฉ ํ•˜๋“œ ์ฝ”๋”ฉํ•ด๋ฒ„๋ฆฐ๋‹ค. ์–ด์ฐจํ”ผ ๋ชฉ์ ์€ Dependent์ธ ViewModel์˜ ๊ฒ€์ฆ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
 

#2-3 Stub (์ •ํ™•ํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” Fake)

// Stub ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•œ Unit test
fun main() {
    // Dependency (Test double) ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val testDouble = StubCalculation()

    // Dependent ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val dependent = Math(testDouble)

    // (radius๊ฐ’์œผ๋กœ 5.0์ด ๋“ค์–ด๊ฐ„๋‹ค๊ณ  ๊ฐ€์ • ํ›„) Unit test ์ง„ํ–‰
    if (dependent.doMathOperation(5.0) == 78.5398) {
        println("ํ…Œ์ŠคํŠธ ์„ฑ๊ณต")
    } else {
        println("ํ…Œ์ŠคํŠธ ์‹คํŒจ")
    }
}

class StubCalculation : Calculation() {
    override fun circleArea(radius: Double): Double {
        return 5 * 5 * 3.141592
    }
}

Fake๋Š” ์ถœ๋ ฅ ๊ทธ ์ž์ฒด์— ์˜๋ฏธ๋ฅผ ๋‘”๋‹ค๋ฉด, Stub๋Š” ์ถœ๋ ฅ๊ฐ’๋„ ๊ฒ€์ฆ ๋Œ€์ƒ์œผ๋กœ ๋ณธ๋‹ค. ์ฆ‰, ์ถœ๋ ฅ๊ฐ’์ด ์ •ํ™•ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ์œ„ ์ฝ”๋“œ์˜ StubCalculation()์€ #3-2์˜ FakeCalculation๊ณผ๋Š” ๋‹ฌ๋ฆฌ ํŒŒ์ด๊ฐ’์„ ๋ถ€๋ชจ ํด๋ž˜์Šค์ธ Calculation()์™€ ๋™์ผํ•œ ๊ฐ’์œผ๋กœ ๋‘๊ณ  ์žˆ๋‹ค. ๋ฌผ๋ก , radius๊ฐ’์€ ์—ฌ์ „ํžˆ ํ•˜๋“œ ์ฝ”๋”ฉ๋˜์–ด์žˆ์ง€๋งŒ ๋ง์ด๋‹ค. Stub์—์„œ ์ค‘์š”ํ•œ ๊ฒƒ์€ Input์œผ๋กœ ๋ญ”๊ฐ€๋ฅผ ๋„ฃ์—ˆ์„ ๋•Œ, ๊ทธ Output์ด Original ํด๋ž˜์Šค์˜ Output๊ณผ ๋™์ผํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด๋‹ค.
 

#2-4 Mock (์ƒํ˜ธ์ž‘์šฉ์„ ๊ฒ€์ฆํ•˜๋Š” Stub)

// Mock ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•œ Unit test
fun main() {
    // Dependency (Test double) ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val testDouble = MockCalculation()

    // Dependent ์ธ์Šคํ„ด์Šค ์„ ์–ธ
    val dependent = Math(testDouble)

    // (radius๊ฐ’์œผ๋กœ 5.0์ด ๋“ค์–ด๊ฐ„๋‹ค๊ณ  ๊ฐ€์ • ํ›„) Unit test ์ง„ํ–‰
    if (dependent.doMathOperation(5.0) == 78.5398) {
        println("์ถœ๋ ฅ๊ฐ’์€ ์ •ํ™•ํ•จ")
        println("ํ›„์† ์ž‘์—… ํ•„์š”: ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ํšŸ์ˆ˜, ์ˆœ์„œ, ๋ฐฉ์‹ ๋“ฑ Original๊ณผ ๊ฐ™์€์ง€ ํ™•์ธ")
    } else {
        println("ํ…Œ์ŠคํŠธ ์‹คํŒจ")
    }
}

class MockCalculation : Calculation() {

    override fun circleArea(radius: Double): Double {
        println("๋กœ๊ทธ: circleArea(5.0) ์ˆ˜ํ–‰")
        return piMultiply(square(5.0))
    }

    override fun square(operand: Double): Double {
        println("๋กœ๊ทธ: 5.0์˜ ์ œ๊ณฑ ์—ฐ์‚ฐ ์ˆ˜ํ–‰")
        return 25.0
    }

    override fun piMultiply(operand: Double): Double {
        println("๋กœ๊ทธ: 25.0 ๊ณฑํ•˜๊ธฐ ํŒŒ์ด ์—ฐ์‚ฐ ์ˆ˜ํ–‰")
        return 78.5398
    }
}

์ด์   ์ƒํ˜ธ์ž‘์šฉ๊นŒ์ง€ ๊ฒ€์ฆํ•œ๋‹ค. Input์˜ ๊ฐ’์€ ์—ฌ์ „ํžˆ ํ•˜๋“œ ์ฝ”๋”ฉ๋˜์ง€๋งŒ, ๊ทธ ๊ฐ’์ด ์ฒ˜๋ฆฌ๋˜๋Š” ๋ฐฉ์‹(๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ํšŸ์ˆ˜, ์ˆœ์„œ, ๋ฐฉ์‹ ๋“ฑ)์ด Original๊ณผ ๋˜‘๊ฐ™์•„์•ผ ํ•œ๋‹ค. Output๋„ ๋‹น์—ฐํžˆ Original์˜ Output๊ณผ ๊ฐ™์•„์•ผ ํ•˜๊ณ  ๋ง์ด๋‹ค. ํ•˜์ง€๋งŒ, ๋ชจ๋“  Mock์„ ์œ„์— ์žˆ๋Š” ์ฝ”๋“œ์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž ์ •์˜ ํด๋ž˜์Šค๋กœ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๊ธด ์‰ฝ์ง€ ์•Š๋‹ค. ํŠนํžˆ ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ๋Š” ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋ ˆ์ž„์›Œํฌ๊นŒ์ง€ ๊ณ ๋ ค๋ฅผ ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์‹ค์ƒ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. ํ•˜์ง€๋งŒ ์˜คํžˆ๋ ค ์ด ๋‹จ์ ์— ์˜ํ•ด ์ˆ˜ ๋งŽ์€ Mock ์ƒ์„ฑ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์ถœ๋ฒ”๋˜์—ˆ๋‹ค.
 

#3 ์š”์•ฝ

Test double์€ Dependent๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ๋”๋ฏธ Dependency๋‹ค.
 

#4 ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ

testDoubleType.zip
0.00MB

Test double์˜ 3๊ฐ€์ง€ ์œ ํ˜•