测试
持续测试对于快速程序开发至关重要。
如果更改代码的某一部分会导致其他代码出现问题,您的测试会立即显示问题。如果您无法立即找出问题,更改会累积起来,您将无法确定哪个更改引起了问题。您将花费更多的时间来追踪问题。
测试是一项至关重要的实践,因此我们在早期引入它,并在本书的其余部分中使用它。通过这种方式,您将习惯将测试作为编程过程的标准部分。
使用 println()
来验证代码正确性是一种薄弱的方法——您必须每次都仔细检查输出,并有意识地确保它是正确的。
为了简化在使用本书时的体验,我们创建了自己的小型测试系统。目标是采用最小的方法:
- 显示表达式的预期结果。
- 提供输出,以便在所有测试成功时,您仍然知道程序正在运行。
- 在您的实践中早早地形成测试的概念。
虽然对于本书非常有用,但我们的测试系统并不是工作场所的测试系统。他人花费了很长时间和精力来创建这些测试系统。例如:
- JUnit 是最受欢迎的 Java 测试框架之一,可以轻松地从 Kotlin 中使用。
- Kotest 是专为 Kotlin 设计的,并利用了 Kotlin 的语言特性。
- Spek 框架 提供了一种不同形式的测试,称为规范测试。
要使用我们的测试框架,首先必须进行import
。框架的基本元素是 eq
(等于)和 neq
(不等于):
// Testing/TestingExample.kt
import atomictest.*
fun main() {
val v1 = 11
val v2 = "Ontology"
// 'eq' 表示 "等于":
v1 eq 11
v2 eq "Ontology"
// 'neq' 表示 "不等于"
v2 neq "Epistimology"
// [Error] Epistimology != Ontology
// v2 eq "Epistimology"
}
/* 输出:
11
Ontology
Ontology
*/
atomictest
包的代码位于 附录 A: AtomicTest。我们并不打算让您现在理解 AtomicTest.kt
中的所有内容,因为它使用了一些在本书后面才会出现的功能。
为了产生整洁、舒适的外观,AtomicTest
使用了 Kotlin 的一个特性,您尚未见过:即在文本样式中以 a function b
的形式写一个函数调用 a.function(b)
。这被称为中缀表示法。只有使用 infix
关键字定义的函数才能以这种方式被调用。AtomicTest.kt
定义了 TestingExample.kt
中使用的 infix
eq
和 neq
:
expression eq expected
expression neq expected
eq
和 neq
是为 AtomicTest
定义的基本(中缀)函数——它真的是一个最小化的测试系统。当您在示例中放置 eq
和 neq
表达式时,您将同时创建一个测试和一些控制台输出。通过运行程序,您可以验证程序的正确性。
AtomicTest
中还有第二个工具。trace
对象用于捕获输出,以供稍后进行比较:
// Testing/Trace1.kt
import atomictest.*
fun main() {
trace("line 1")
trace(47)
trace("line 2")
trace eq """
line 1
47
line 2
"""
}
将结果添加到 trace
类似于一个函数调用,因此您可以将 println()
效果地替换为 trace()
。
在之前的内容中,我们显示了输出并依赖于人类视觉检查以捕获任何差异。这是不可靠的;即使在我们一遍又一遍地检查代码的书中,我们也发现视觉检查无法被信任地发现错误。从现在开始,我们很少使用带有注释的输出块,因为 AtomicTest
将为我们执行所有操作。然而,有时当这会产生更有用的效果时,我们仍然会包含带有注释的输出块。
通过在本书的其余部分看到在测试中使用测试的好处,您应该能够将测试融入到您的编程过程中。当您看到没有测试的代码时,您可能会开始感到不安。您甚至可能会认为没有测试的代码在本质上是错误的。
测试作为编程的一部分
测试在将其内置到软件开发过程中时效果最佳。编写测试确保您获得预期的结果。许多人主张在编写实现代码之前编写测试——在编写代码使其通过之前,您首先使测试失败。这种称为测试驱动开发(Test Driven Development,TDD)的技术是一种确保您真正测试您认为要测试的内容的方法。您可以在维基百科上找到更完整的 TDD 描述(搜索“Test Driven Development”)。
编写可测试的代码还有另一个好处——它改变了您编写代码的方式。您可以仅仅在控制台上显示结果。但是在测试思维中,您会想知道,“我将如何测试这个?”当您创建一个函数时,您会决定应该从函数中返回一些东西,即使只是为了测试该结果。只处理输入并生成输出的函数往往会生成更好的设计。
以下是使用 TDD 来实现 Number Types 中的 BMI 计算的一个简化示例。首先,我们编写测试,以及一个最初的实现,该实现失败(因为我们尚未实现功能):
// Testing/TDDFail.kt
package testing1
import atomictest.eq
fun main() {
calculateBMI(160, 68) eq "Normal weight"
// calculateBMI(100, 68) eq "Underweight"
// calculateBMI(200, 68) eq "Overweight"
}
fun calculateBMI(lbs: Int, height: Int) =
"Normal weight"
只有第一个测试通过。其他测试失败并被注释掉了。接下来,我们添加了代码来确定哪些体重属于哪些分类。现在所有的测试都失败了:
// Testing/TDDStillFails.kt
package testing2
import atomictest.eq
fun main() {
// 一切都失败了:
// calculateBMI(160, 68) eq "Normal weight"
// calculateBMI(100, 68) eq "Underweight"
// calculateBMI(200, 68) eq "Overweight"
}
fun calculateBMI(
lbs: Int,
height: Int
): String {
val bmi = lbs / (height * height) * 703.07
return if (bmi < 18.5) "Underweight"
else if (bmi < 25) "Normal weight"
else "Overweight"
}
我们使用 Int
而不是 Double
,导致结果为零。测试指导我们进行修复:
// Testing/TDDWorks.kt
package testing3
import atomictest.eq
fun main() {
calculateBMI(160.0, 68.0) eq "Normal weight"
calculateBMI(100.0, 68.0) eq "Underweight"
calculateBMI(200.0, 68.0) eq "Overweight"
}
fun calculateBMI(
lbs: Double,
height: Double
): String {
val bmi = lbs / (height * height) * 703.07
return if (bmi < 18.5) "Underweight"
else if (bmi < 25) "Normal weight"
else "Overweight"
}
您可以选择为边界条件添加其他测试。
在本书的练习中,我们包含了您的代码必须通过的测试。
练习和解答可以在 www.AtomicKotlin.com 找到。