// 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"
}