Android单元测试基础
单元测试用于验证应用中最小单元(函数或类 )的行为是否正确。在 Android/Kotlin 项目中,本地单元测试通常放在 module/src/test/
目录下,使用 JUnit4 框架编写。要启用测试,需要在 Gradle 中添加依赖,例如
testImplementation "junit:junit:版本号"
(JUnit4)和testImplementation "io.mockk:mockk:版本号"
(MockK)。
每个测试类中包含一个或多个用 @Test
注解标记的方法,在方法体内调用被测函数并使用断言验证输出。
kotlin
class EmailValidatorTest {
@Test
fun emailValidator_CorrectEmailSimple_ReturnsTrue() {
assertTrue(EmailValidator.isValidEmail("[email protected]"))
}
}
该示例中,测试方法通过 assertTrue
检查 isValidEmail()
的返回值。在编写单元测试时,我们通常将外部依赖(如数据库、网络、Android 框架等)替换为可控的测试替身(如 mock 对象),以实现隔离测试。常用的断言库包括 JUnit Assert、Hamcrest、Truth 等。
测试替身
单元测试中的测试替身(Test Doubles)
依赖隔离,核心逻辑:
关键概念解析
替身类型 | 作用场景 | 示例 |
---|---|---|
Mock | 验证交互行为 | 检查是否调用了数据库API |
Stub | 返回预设数据 | 固定返回用户{id:1, name:测试} |
Fake | 简化实现替代真实服务 | 内存数据库替代MySQL |
Spy | 记录调用信息(Mock的变体) | 记录网络请求次数 |
Dummy | 填充参数(无逻辑) | new Object()占位 |
在Android测试中的典型应用
java
// 使用Mockito框架示例
@Mock
Database mockDB; // 创建数据库Mock
@Test
public void testUserSave() {
// 1. 设置Stub行为
when(mockDB.save(any(User.class))).thenReturn(true);
// 2. 执行被测方法
service.saveUser(new User("test"));
// 3. 验证Mock交互
verify(mockDB).save(any(User.class));
}
核心价值
- 🛡️ 隔离性:避免测试因网络/DB故障而失败
- ⚡ 加速测试:移除真实IO操作(原需200ms→2ms)
- 🔍 行为验证:确保调用次数/参数符合预期
- 🧪 边界覆盖 :轻松模拟异常场景(如:
when(...).thenThrow(...)
)
MockK 概念与常用用法
MockK 是一个专为 Kotlin 设计的模拟测试框架,相比 Mockito 等 Java 库,MockK 自然支持 Kotlin 的特性,如 final 类、扩展函数和协程。使用 MockK 可以方便地创建接口或类的 mock 对象,并通过 DSL 定义其行为。最简单的使用方法如下:
kotlin
val car = mockk<Car>() // 创建 Car 类的 mock 对象
every { car.drive(Direction.NORTH) } returns Outcome.OK // 定义方法返回值
car.drive(Direction.NORTH) // 调用时返回 OK
verify { car.drive(Direction.NORTH) } // 验证方法被调用
confirmVerified(car)
以上示例来自 MockK 官方文档。其中 mockk<T>()
会创建一个 严格模式 (strict)的 mock 对象,需要显式定义其所有行为(使用 every { ... } returns ...
)。
什么叫做严格模式
1. 什么是严格模式?
- 在 MockK 中,严格模式 (也称为标准模式)意味着 mock 对象会严格执行以下规则:
- 所有对 mock 对象方法的调用都必须预先声明 (即使用
every
块定义行为)。 - 如果调用了一个没有预先声明的方法,MockK 会立即抛出异常(
MockKException: no answer found
)。
- 所有对 mock 对象方法的调用都必须预先声明 (即使用
例如:
kotlin
val car = mockk<Car>() // 创建严格模式的 mock 对象
// 未定义行为时调用方法 → 抛出异常!
car.drive(50) // 抛出 no answer found 异常
2. 显式定义行为
使用 every { ... } returns ...
结构为 mock 对象的方法定义行为:
kotlin
every { car.drive(50) } returns "Driving at 50 km/h"
every
块:声明一个预期调用的行为。returns
:指定该方法调用的返回值。
此时调用 car.drive(50)
会返回 "Driving at 50 km/h"
。
3. 为何需要显式定义所有行为?
- 避免隐藏错误:严格模式强制测试编写者明确指定 mock 对象的所有预期行为。这有助于暴露测试中的隐含假设或遗漏的依赖调用。
- 提高测试可靠性:确保测试只关注预先定义的行为,避免因意外调用导致的假阳性/假阴性结果。
4. 未定义行为的后果
如果在严格模式下调用未定义的方法,会收到如下错误:
css
io.mockk.MockKException: no answer found for: Car(#1).drive(50)
5. 对比:严格模式 vs 松弛模式
模式 | 是否需要显式定义行为 | 未定义行为时的处理 |
---|---|---|
严格模式 | 是 | 抛出异常 |
松弛模式 | 否 | 返回默认值(null, 0 等) |
松弛模式的创建方式:
kotlin
val relaxedCar = mockk<Car>(relaxed = true) // 不会因未定义行为抛出异常
6. 何时使用严格模式?
-
推荐场景 :
- 需要精确控制 mock 行为的测试。
- 验证代码是否按预期调用了特定方法(通常结合
verify
)。
-
不推荐场景 :当 mock 对象有许多不重要的方法(如纯数据模型),使用严格模式会写大量
every
块,此时可改用松弛模式。
7. 完整示例
kotlin
// 定义类
class Car {
fun drive(speed: Int): String = "Real driving: $speed km/h"
}
// 测试
@Test
fun testStrictMock() {
// 1. 创建严格模式 mock
val car = mockk<Car>()
// 2. 显式定义行为
every { car.drive(50) } returns "Mocked driving at 50 km/h"
// 3. 调用已定义方法 → 成功
assertEquals("Mocked driving at 50 km/h", car.drive(50))
// 4. 调用未定义方法 → 抛出异常!
assertFailsWith<MockKException> {
car.drive(100) // 未定义 100 的行为
}
}
总结
mockk<T>()
创建的是严格模式的 mock 对象。- 必须 用
every { ... } returns ...
为其每个需要调用的方法定义行为。 - 严格模式能提高测试的精确性,但会增加样板代码。根据场景选择是否使用松弛模式(
relaxed = true
)。### 详细解释:mockk<T>()
创建严格模式(Strict Mode)的 Mock 对象
核心概念:严格模式 (Strict Mode)
kotlin
val myService = mockk<MyService>() // 创建严格模式的 mock 对象
-
行为必须显式定义
- 在严格模式下,mock 对象的所有交互行为都必须预先声明
- 如果调用了未定义的方法,会立即抛出异常:
io.mockk.MockKException: no answer found for: MyService(#1).getData()
-
定义行为的方式
使用
every { ... } returns ...
结构显式声明行为:kotlin// 必须为每个需要调用的方法定义行为 every { myService.getData() } returns "MockedData" every { myService.calculate(any()) } returns 42
为什么需要严格模式?
场景 | 严格模式 | 非严格模式 |
---|---|---|
未定义方法调用 | 立即抛出异常 | 返回默认值(null, 0 等) |
测试可靠性 | 确保不会意外调用未模拟的方法 | 可能隐藏未覆盖的依赖 |
测试意图 | 明确声明所有预期行为 | 隐含接受默认行为 |
典型错误示例
kotlin
// 测试代码
val userService = mockk<UserService>() // 严格模式
// ❌ 忘记定义行为
userService.findUser("id123") // 抛出 MockKException!
// ✅ 正确做法
every { userService.findUser(any()) } returns User("MockedUser")
val result = userService.findUser("id123") // 返回 User 对象
严格模式的核心特点
-
零容忍未声明行为
任何未通过
every
定义的调用都会导致测试失败。 -
精确控制模拟行为
必须为每个参数组合指定行为:
kotlin// 不同参数需要单独定义 every { myService.parse("A") } returns 1 every { myService.parse("B") } returns 2
-
与验证的配合
常与
verify
一起使用确保调用符合预期:kotlinevery { myService.send(any()) } returns true // 测试代码 myService.send("message") verify { myService.send("message") } // 验证调用发生
何时使用严格模式?
-
推荐场景
- 需要精确控制依赖行为的测试
- 验证复杂交互逻辑
- 关键服务/组件的测试
-
替代方案(非严格模式)
kotlinval relaxedService = mockk<MyService>(relaxed = true) // 允许未定义调用
Mock 接口、类、静态和扩展方法
-
接口/类的 mock: 对于普通的接口或类,使用
mockk<类型>()
创建 mock 对象。例如val repo = mockk<MyRepository>()
。注意 MockK 能直接 mock Kotlin 中的final class
,无需像 Mockito 那样特殊配置。也可以使用注解@MockK
并在测试@Before
中调用MockKAnnotations.init(this)
或使用前述的MockKRule
进行初始化。 -
Relaxed Mock: 如果不想为每个方法都定义返回值,可以创建一个 relaxed mock,即
mockk<MyClass>(relaxed = true)
。这会让所有非 Unit 返回类型的方法自动返回默认值(例如布尔型为 false,引用型为 null)。这样即使不显式 stub 方法,调用时也不会抛异常。需要注意,针对泛型返回类型的函数,放宽 mock 有时可能抛出类型转换异常。 -
静态方法和顶层函数: Kotlin 的顶层函数 或 Java 静态方法可以用
mockkStatic()
模拟。对于 Java 静态方法或类静态方法,直接传入类引用:kotlinmockkStatic(Uri::class) every { Uri.parse("http://test/path") } returns mockUri
这会拦截
Uri.parse()
的调用,返回自定义结果。在模拟 Kotlin 顶层(module-wide)函数或扩展函数时,可以传入生成的类名字符串(通常是包名+文件名+Kt
后缀)或函数引用。例如文档中示例,将扩展函数所在文件File.kt
(包名为 pkg)中所有函数静态化:kotlinmockkStatic("pkg.FileKt") every { Obj(5).extensionFunc() } returns 11
如此可以模拟
Obj.extensionFunc()
的行为。总之,mockkStatic
适用于替换任何静态或顶层函数的实现,模拟完成后可用unmockkStatic
解除。 -
对象(单例)的 mock: 对于 Kotlin 的
object
或 Java 的单例,可使用mockkObject(SomeObject)
创建 mock 对象。此时可以像普通 mock 一样用every { ... }
定义行为,并在测试后调用unmockkObject(SomeObject)
恢复原状。 -
注意事项: 使用
mockkStatic
或mockkObject
后要记得在测试结束时使用unmockkStatic
或unmockkObject
清理,否则可能影响后续测试。