Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

测试

持续测试对于快速程序开发至关重要。

如果更改代码的某一部分会导致其他代码出现问题,您的测试会立即显示问题。如果您无法立即找出问题,更改会累积起来,您将无法确定哪个更改引起了问题。您将花费更多的时间来追踪问题。

测试是一项至关重要的实践,因此我们在早期引入它,并在本书的其余部分中使用它。通过这种方式,您将习惯将测试作为编程过程的标准部分。

使用 println() 来验证代码正确性是一种薄弱的方法——您必须每次都仔细检查输出,并有意识地确保它是正确的。

为了简化在使用本书时的体验,我们创建了自己的小型测试系统。目标是采用最小的方法:

  1. 显示表达式的预期结果。
  2. 提供输出,以便在所有测试成功时,您仍然知道程序正在运行。
  3. 在您的实践中早早地形成测试的概念。

虽然对于本书非常有用,但我们的测试系统并是工作场所的测试系统。他人花费了很长时间和精力来创建这些测试系统。例如:

  • 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 eqneq

expression eq expected
expression neq expected

eqneq 是为 AtomicTest 定义的基本(中缀)函数——它真的是一个最小化的测试系统。当您在示例中放置 eqneq 表达式时,您将同时创建一个测试和一些控制台输出。通过运行程序,您可以验证程序的正确性。

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 找到。