μSpek Playground



// Example "very complicated system" to test (with mutable state) :-)

class MicroCalc(var result: Int) {
    fun add(x: Int) { result += x }
    fun multiplyBy(x: Int) { result *= x }
}




// Example tests

fun main() = microCalcTest()

fun microCalcTest() = uspek {

    "create SUT" o {

        val sut = MicroCalc(10)

        "check add" o {
            sut.add(5)
            sut.result eq 15
            sut.add(100)
            sut.result eq 115
        }

        "mutate SUT" o {
            sut.add(1)

            "incorrectly check add - this should fail" ox {
                sut.add(5)
                sut.result eq 15
            }
        }

        "check add again" o {
            sut.add(5)
            sut.result eq 15
            sut.add(100)
            sut.result eq 115
        }

        testSomeAdding(sut)

        "mutate SUT and check multiplyBy" o {
            sut.result = 3

            sut.multiplyBy(3)
            sut.result eq 9
            sut.multiplyBy(4)
            sut.result eq 36

            testSomeAdding(sut)
        }

        "assure that SUT is intact by any of sub tests above" o {
            sut.result eq 10
        }
    }
}

private fun testSomeAdding(calc: MicroCalc) {
    val start = calc.result
    "add 5 to $start" o {
        calc.add(5)
        val afterAdd5 = start + 5
        "result should be $afterAdd5" o { calc.result eq afterAdd5 }

        "add 7 more" o {
            calc.add(7)
            val afterAdd5Add7 = afterAdd5 + 7
            "result should be $afterAdd5Add7" o { calc.result eq afterAdd5Add7 }
        }
    }

    "subtract 3" o {
        calc.add(-3)
        val afterSub3 = start - 3
        "result should be $afterSub3" o { calc.result eq afterSub3 }
    }
}



// The whole uSpek test framework implementation

fun uspek(code: () -> Unit) {
    while (true) try {
        uspekContext.branch = uspekContext.root
        code()
        break
    } catch (e: USpekException) {
        uspekContext.branch.end = e
        uspekLog(uspekContext.branch)
    }
}

infix fun String.o(code: () -> Unit) {
    val subbranch = uspekContext.branch.branches.getOrPut(this) { USpekTree(this) }
    subbranch.end === null || return // already tested so skip this whole subbranch
    uspekContext.branch = subbranch // step through the tree into the subbranch
    uspekLog(subbranch)
    throw try { code(); USpekException() }
    catch (e: USpekException) { e }
    catch (e: Throwable) { USpekException(e) }
}

@Suppress("UNUSED_PARAMETER")
@Deprecated("Enable this test code", ReplaceWith("o(code)"))
infix fun String.ox(code: () -> Unit) = Unit

data class USpekContext(
    val root: USpekTree = USpekTree("uspek"),
    var branch: USpekTree = root
)

val uspekContext = USpekContext()

data class USpekTree(
    val name: String,
    val branches: MutableMap<String, USpekTree> = mutableMapOf(),
    var end: USpekException? = null,
    var data: Any? = null
)

class USpekException(cause: Throwable? = null) : RuntimeException(cause)

var uspekLog: (USpekTree) -> Unit = { println(it.status) }

val USpekTree.status get() = when {
    failed -> "FAILURE.($location)\nBECAUSE.($causeLocation)\n"
    finished -> "SUCCESS.($location)\n"
    else -> name
}

val USpekTree.finished get() = end !== null

val USpekTree.failed get() = end?.cause !== null

val USpekTree?.location get() = this?.end?.stackTrace?.userCall?.location

val USpekTree?.causeLocation get() = this?.end?.causeLocation

typealias StackTrace = Array<StackTraceElement>

infix fun <T> T.eq(expected: T) = check(this == expected) { "$this != $expected" }


data class CodeLocation(val fileName: String, val lineNumber: Int) {
    override fun toString() = "$fileName:$lineNumber"
}

val StackTraceElement.location get() = CodeLocation(fileName, lineNumber)

val Throwable.causeLocation: CodeLocation?
    get() {
        val file = stackTrace.getOrNull(1)?.fileName
        val frame = cause?.stackTrace?.find { it.fileName == file }
        return frame?.location
    }

val StackTrace.userCall get() = findUserCall()?.let(::getOrNull)

private fun StackTrace.findUserCall() = (1 until size).find {
    this[it - 1].fileName == "USpek.kt" && this[it].fileName != "USpek.kt"
}