第3章:第一个kotlin.test单元测试:@Test注解与断言
终于到了动手写测试的时候了。这一章,我们就用 kotlin.test 写第一个单元测试。说白了,就是学会两样东西:@Test 注解怎么用,以及断言怎么写。别小看这两步,我见过不少新人在这上面翻车。
3.1 @Test 注解:告诉框架“这是测试”
在 JUnit 里,测试方法必须标记 @Test。kotlin.test 也一样,只不过它用的是自己的注解。不过说实话,kotlin.test 的 @Test 实际来自 kotlin.test.Test,别和 JUnit 的搞混了——虽然它们长得一样,但包不同。
来看看最基础的写法:
import kotlin.test.Test
class MyFirstTest {
@Test
fun shouldPass() {
// 测试逻辑
}
}
嗯,就这么简单。但有个细节要注意:测试方法必须是 fun,不能是属性,也不能带参数。我曾经犯过一个低级错误,在测试方法里加了参数,结果编译通过但运行时报错找不到测试方法——排查了半天,你说冤不冤?
3.2 断言家族:验证结果的武器库
光有 @Test 没断言,测试等于没写。kotlin.test 提供了一套简洁的断言函数,咱们先把常用的列出来:
| 断言函数 | 作用 | 示例 |
|---|---|---|
assertEquals |
判断两个值是否相等 | assertEquals(4, 2 + 2) |
assertNotEquals |
判断两个值不相等 | assertNotEquals(5, 2 + 2) |
assertTrue |
判断表达式为 true | assertTrue(list.isEmpty()) |
assertFalse |
判断表达式为 false | assertFalse(list.isNotEmpty()) |
assertNull |
判断值为 null | assertNull(result) |
assertNotNull |
判断值不为 null | assertNotNull(name) |
assertSame |
判断两个引用为同一对象 | assertSame(a, b) |
assertNotSame |
判断引用不同 | assertNotSame(a, b) |
fail |
强制失败,常用于异常分支 | fail("不应该走到这里") |
你想想看,这么多断言,日常用最多的是哪几个?我个人习惯是 assertEquals 和 assertTrue 占了八成场景。其他的偶尔用到,但也得心里有数。
3.3 完整实例:从零跑通第一个测试
咱们实际写一个能运行的例子。假设有个计算器类:
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun divide(a: Int, b: Int): Int {
if (b == 0) throw IllegalArgumentException("除数不能为0")
return a / b
}
}
对应的测试代码:
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class CalculatorTest {
private val calculator = Calculator()
@Test
fun `加法运算正确`() {
val result = calculator.add(2, 3)
assertEquals(5, result)
}
@Test
fun `除数不能为零`() {
assertFailsWith<IllegalArgumentException> {
calculator.divide(10, 0)
}
}
}
注意,这里我用了反引号函数名。Kotlin 允许方法名带空格和中文,测试方法名写清楚意图非常实用。我在项目中一直用这个方式,不用再写 @DisplayName 了,清爽很多。
核心要点
- 测试类不需要继承任何基类,也不需要 public
- 测试方法必须用 @Test 标记
- 断言函数是验证结果的唯一方式,记得每个测试至少有一个断言
3.4 断言的“隐式”消息与自定义消息
你可能注意到了,assertEquals 失败时,框架会自动打印出期望值和实际值。但有时候这不够——比如你测试一个复杂对象的字段,光看数字对不上,得知道是哪个字段错了。
kotlin.test 允许你传一个 message 参数作为失败时的额外说明:
@Test
fun `带消息的断言`() {
val actual = "Kotlin"
val expected = "Kotlin Test"
assertEquals(expected, actual, "字符串内容不一致")
}
不过我个人建议,不要每个断言都加消息,只有当你觉得失败信息不够清晰时才加。消息写多了,代码会变得又臭又长。
3.5 异常测试:assertFailsWith 的妙用
前面已经用到 assertFailsWith 了。这个函数专门用来验证某段代码是否抛出了指定异常。如果你用过 JUnit 的 expected 或者 assertThrows,那上手就更容易了。
它有两种写法:
// 写法一:lambda 块
assertFailsWith<IllegalArgumentException> {
calculator.divide(1, 0)
}
// 写法二:捕获异常对象,做进一步验证
val exception = assertFailsWith<IllegalArgumentException> {
calculator.divide(1, 0)
}
assertEquals("除数不能为0", exception.message)
写法二可以拿到异常实例,然后断言它的 message 或其他属性。这个很实用——有时候你不仅要确认抛了异常,还得确认异常信息对不对。
我曾经踩过的坑:
用 assertFailsWith 时,lambda 里不要写 try-catch 把异常吞掉。我曾见过有人这么干:
assertFailsWith<Exception> {
try {
riskyCall()
} catch (e: Exception) {
// 什么都不做
}
}
这会导致测试永远不失败,因为异常被吞了,assertFailsWith 根本捕获不到。记住:让异常自然抛出就好。
3.6 测试方法的访问级别与可见性
Kotlin 中,方法默认是 public 的。所以测试方法不需要写 public 修饰符。但要注意一点:如果测试方法用了 internal 或 private,Gradle 或 Maven 里的测试任务可能找不到它。
这又是一个血泪教训。有次我用 internal 修饰了测试方法,结果 IDE 里能运行,但命令行 Gradle 构建报 No tests found。排查了半天,最后发现是可见性问题。所以老老实实别加修饰符,或者显式写成 public 都可以。
3.7 测试类与测试方法命名规范
虽然不是强制,但我建议形成一套团队规范。常见做法:
- 测试类名:被测试类名 + Test 后缀,例如
Calculator→CalculatorTest - 测试方法名:用反引号包裹描述性中文或英文,例如
`加法运算正确`或`should_return_5_when_add_2_and_3`
这样做的好处是:测试失败时,从报告里一眼就能看出哪个用例挂了,不用再去查代码。
技巧:如果你在 IntelliJ IDEA 里运行测试,方法名会直接显示在 Run 窗口里。用中文方法名特别清晰,团队里用中文的同事更容易理解。
知识体系图:@Test + 断言核心流程
下面这张图总结了本章涉及的核心概念和它们之间的关系:
整个流程很清晰:你用一个 @Test 标记的方法执行待测代码,然后通过断言函数来验证结果是否符合预期。断言失败时,框架会抛出 AssertionError 并报告位置。这就是单元测试最核心的运作机制。
3.8 本章小结:你只需要记住这三句话
说了这么多,其实就三条硬道理:
- 每个测试方法都要写
@Test,别漏了,别用错包。 - 至少有一个断言,否则测试毫无意义。
- 异常测试用
assertFailsWith,别 try-catch 把异常吞掉。