单元测试
单元测试是为函数的每个方面创建正确性测试的实践。单元测试可以快速地揭示出错误的代码,加快开发速度。
关于测试的内容远远超出了这本书的范围,所以本文只是一个基本的介绍。
"单元" 在 "单元测试" 中描述了一个小的代码片段,通常是一个函数,它被单独且独立地测试。这不应与不相关的 Kotlin Unit
类型混淆。
单元测试通常由程序员编写,并在每次构建项目时运行。由于单元测试运行频率非常高,因此它们必须运行得很快。
在阅读本书时,您已经了解了单元测试,通过我们用来验证书中代码的 AtomicTest
库。AtomicTest
使用简洁的 eq
函数来完成单元测试中最常见的模式:将预期结果与生成的结果进行比较。
在众多的单元测试框架中,JUnit 是 Java 中最流行的。还有专门为 Kotlin 创建的框架。Kotlin 标准库中包含了 kotlin.test
,它提供了不同测试库的外观。这样,您不会受限于使用特定的库。kotlin.test
还包含了基本断言函数的封装。
要使用 kotlin.test
,您必须修改项目的 build.gradle
文件的 dependencies
部分,包括:
testImplementation "org.jetbrains.kotlin:kotlin-test-common"
在单元测试中,程序员调用各种断言函数,以验证待测试函数的预期行为。断言函数包括 assertEquals()
,它将实际值与预期值进行比较,以及 assertTrue()
,它测试其第一个参数,一个布尔表达式。在此示例中,单元测试是以单词 test
开头的函数:
// UnitTesting/NoFramework.kt
package unittesting
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import atomictest.*
fun fortyTwo() = 42
fun testFortyTwo(n: Int = 42) {
assertEquals(
expected = n,
actual = fortyTwo(),
message = "Incorrect,")
}
fun allGood(b: Boolean = true) = b
fun testAllGood(b: Boolean = true) {
assertTrue(allGood(b), "Not good")
}
fun main() {
testFortyTwo()
testAllGood()
capture {
testFortyTwo(43)
} contains
listOf("expected:", "<43>",
"but was", "<42>")
capture {
testAllGood(false)
} contains listOf("Error", "Not good")
}
在 main()
中,您可以看到一个失败的断言函数会产生一个 AssertionError
- 这意味着单元测试失败,将问题通知给程序员。
kotlin.test
包含一系列名称以 assert
开头的函数:
assertEquals()
,assertNotEquals()
assertTrue()
,assertFalse()
assertNull()
,assertNotNull()
assertFails()
,assertFailsWith()
类似的函数通常包含在每个单元测试框架中,但名称和参数顺序可能不同。例如,在 assertEquals()
中的 message
参数可能在第一个或最后一个。此外,很容易混淆 expected
和 actual
- 使用命名参数可以避免这个问题。
kotlin.test
中的 expect()
函数运行一个代码块,并将该结果与预期值进行比较:
fun <T> expect(
expected: T,
message: String?,
block: () -> T
) {
assertEquals(expected, block(), message)
}
这里将 testFortyTwo()
重写为使用 expect()
:
// UnitTesting/UsingExpect.kt
package unittesting
import atomictest.*
import kotlin.test.*
fun testFortyTwo2(n: Int = 42) {
expect(n, "Incorrect,") { fortyTwo() }
}
fun main() {
testFortyTwo2()
capture {
testFortyTwo2(43)
} contains
listOf("expected:",
"<43> but was:", "<42>")
assertFails { testFortyTwo2(43) }
capture {
assertFails { testFortyTwo2() }
} contains
listOf("Expected an exception",
"to be thrown",
"but was completed successfully.")
assertFailsWith<AssertionError> {
testFortyTwo2(43)
}
capture {
assertFailsWith<AssertionError> {
testFortyTwo2()
}
} contains
listOf("Expected an exception",
"to be thrown",
"but was completed successfully.")
}
为了添加对边界情况的测试,是很重要的。如果一个函数在某些条件下产生错误,那么这应该通过单元测试进行验证(就像 AtomicTest
的 capture()
那样)。assertFails()
和 assertFailsWith()
确保异常被抛出。assertFailsWith()
还会检查异常的类型。
测试框架
一个典型的测试框架包含一系列断言函数和运行测试并显示结果的机制。大多数测试运行器会用绿色表示成功,用红色表示失败。
本文将 JUnit5 作为 kotlin.test
的底层库
。要将其包含在项目中,您的 build.gradle
的 dependencies
部分应如下所示:
testImplementation "org.jetbrains.kotlin:kotlin-test"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit5"
testImplementation "org.junit.jupiter:junit-jupiter:$junit_version"
如果您使用不同的库,您可以在该框架的说明中找到设置详细信息。
kotlin.test
提供了最常用函数的外观。断言委托给底层测试框架中的适当函数。例如,在 org.junit.jupiter.api.Assertions
类中,assertEquals()
调用了 Assertions.assertEquals()
。
Kotlin 支持用于定义和表达式的 注解。注解是 @
后跟注解名称的符号,表示对注解元素的特殊处理。@Test
注解将普通函数转换为测试函数。我们可以使用 @Test
注解来测试 fortyTwo()
和 allGood()
:
// Tests/unittesting/SampleTest.kt
package unittesting
import kotlin.test.*
class SampleTest {
@Test
fun testFortyTwo() {
expect(42, "Incorrect,") { fortyTwo() }
}
@Test
fun testAllGood() {
assertTrue(allGood(), "Not good")
}
}
kotlin.test
使用 typealias
来创建 @Test
注解的外观:
typealias Test = org.junit.jupiter.api.Test
这告诉编译器将 @org.junit.jupiter.api.Test
注解替换为 @Test
。
一个测试类通常包含多个单元测试。理想情况下,每个单元测试只验证一个行为。当引入新功能时,每个单元测试不仅会添加新的测试来检查其正确性,还会运行所有现有的测试,以确保之前的功能仍然有效。在引入新变更时,您会感到更加安全,系统也更加可预测和稳定。
在修复新错误的过程中,您会为此和类似情况创建额外的单元测试,以便将来不会犯同样的错误。
如果您使用连续集成(CI)服务器,例如 Teamcity,所有可用的测试都会自动运行,并且如果出现问题,您会收到通知。
考虑一个具有多个属性的类:
// UnitTesting/Learner.kt
package unittesting
enum class Language {
Kotlin, Java, Go, Python, Rust, Scala
}
data class Learner(
val id: Int,
val name: String,
val surname: String,
val language: Language
)
通常,在测试中添加用于制造测试数据的实用函数是有帮助的,尤其是当您在测试期间必须使用相同默认值创建多个对象时。在这里,makeLearner()
创建具有默认值的对象:
// Tests/unittesting/LearnerTest.kt
package unittesting
import unittesting.Language.*
import kotlin.test.*
fun makeLearner(
id: Int,
language: Language = Kotlin, // [1]
name: String = "Test Name $id",
surname: String = "Test Surname $id"
) = Learner(id, name, surname, language)
class LearnerTest {
@Test
fun `single Learner`() {
val learner = makeLearner(10, Java)
assertEquals("Test Name 10", learner.name)
}
@Test
fun `multiple Learners`() {
val learners = (1..9).map(::makeLearner)
assertTrue(
learners.all { it.language == Kotlin })
}
}
在 Learner
中添加默认参数,仅用于测试,会引入不必要的复杂性和潜在的混淆。在生成测试实例时,使用 makeLearner()
更简单、更干净,消除了冗余代码。
makeLearner()
参数的顺序简化了其使用。在这种情况下,我们预计更经常为 lang
指定一个非默认值,而不是更改 name
和 surname
的默认测试值,因此 lang
参数是第二个参数([1])。
模拟和集成测试
依赖于其他组件的系统会使得创建隔离测试变得复杂。程序员通常会使用一种称为 模拟 的实践,而不是引入对真实组件的依赖。
在测试期间,模拟将真实实体替换为虚假实体。通常会对数据库进行模拟,以保持存储数据的完整性。模拟可以实现与真实组件相同的接口,也可以使用模拟库(如 MockK)来创建。
在测试时,重要的是独立地测试不同的功能片段 - 这就是单元测试所做的。同时,还需要确保系统的不同部分在与其他部分组合时能够正常工作 - 这就是 集成测试 所做的。单元测试是“内向”测试,而集成测试是“外向”测试。
在 IntelliJ IDEA 中进行测试
IntelliJ IDEA 和 Android Studio 支持创建和运行单元测试。
要创建测试,请右键单击(Mac 上为控制键点击)您想要测试的类或函数,并从弹出菜单中选择 "Generate..."。从 "Generate" 菜单中选择 "Test..."。第二种方法是打开 “意图操作” 列表,然后选择 “创建测试”。
将 "Testing library" 设置为 JUnit5。如果出现 "在模块中未找到 JUnit5 库" 的消息,请单击消息旁边的 "修复" 按钮。"Destination package"
应为 unittesting
。结果将放在另一个目录中(始终将测试代码与主要代码分开)。Gradle 的默认设置是 src/test/kotlin
文件夹,但您可以选择其他目标。
选中您想要测试的函数旁边的复选框。您可以自动从源代码导航到相应的测试类,反之亦然;有关详细信息,请参阅文档。
生成测试框架代码后,您可以修改它以适应您的需求。在本文的示例和练习中,将:
import org.junit.Test
import org.junit.Assert.*
替换为:
import kotlin.test.*
在 IntelliJ IDEA 中运行测试时,您可能会收到类似于 "未接收到测试事件" 的错误消息。这是因为 IDEA 的默认配置假定您是在外部运行测试,使用 Gradle。要修复这个问题,使您可以在 IDEA 内运行测试,请从文件菜单开始:
File | Settings | Build, Execution, Deployment | Build Tools | Gradle
在该页面上,您将看到一个下拉列表,标题为 "Run tests using:",默认设置为 "Gradle (Default)"。将其更改为 "IntelliJ IDEA",您的测试将正确运行。
练习和解答可以在 www.AtomicKotlin.com 找到。