Android单元测试

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("name@example.com"))
    }
}

该示例中,测试方法通过 assertTrue 检查 isValidEmail() 的返回值。在编写单元测试时,我们通常将外部依赖(如数据库、网络、Android 框架等)替换为可控的测试替身(如 mock 对象),以实现隔离测试。常用的断言库包括 JUnit Assert、Hamcrest、Truth 等。

测试替身

单元测试中的测试替身(Test Doubles)

依赖隔离,核心逻辑:

graph TD A[被测对象] --> B[真实依赖] A --> C[测试替身] C --> D[Mock对象] C --> E[Stub] C --> F[Fake]

关键概念解析

替身类型 作用场景 示例
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)。

例如:

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. 何时使用严格模式?
  • 推荐场景

    1. 需要精确控制 mock 行为的测试。
    1. 验证代码是否按预期调用了特定方法(通常结合 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 对象
  1. 行为必须显式定义

    • 在严格模式下,mock 对象的所有交互行为都必须预先声明
    • 如果调用了未定义的方法,会立即抛出异常:
      io.mockk.MockKException: no answer found for: MyService(#1).getData()
  2. 定义行为的方式

    使用 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 对象

严格模式的核心特点
  1. 零容忍未声明行为

    任何未通过 every 定义的调用都会导致测试失败。

  2. 精确控制模拟行为

    必须为每个参数组合指定行为:

    kotlin 复制代码
    // 不同参数需要单独定义
    every { myService.parse("A") } returns 1
    every { myService.parse("B") } returns 2
  3. 与验证的配合

    常与 verify 一起使用确保调用符合预期:

    kotlin 复制代码
    every { myService.send(any()) } returns true
    
    // 测试代码
    myService.send("message")
    
    verify { myService.send("message") }  // 验证调用发生

何时使用严格模式?
  • 推荐场景

    • 需要精确控制依赖行为的测试
    • 验证复杂交互逻辑
    • 关键服务/组件的测试
  • 替代方案(非严格模式)

    kotlin 复制代码
    val 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 静态方法或类静态方法,直接传入类引用:

    kotlin 复制代码
    mockkStatic(Uri::class)
    every { Uri.parse("http://test/path") } returns mockUri

    这会拦截 Uri.parse() 的调用,返回自定义结果。在模拟 Kotlin 顶层(module-wide)函数或扩展函数时,可以传入生成的类名字符串(通常是包名+文件名+Kt后缀)或函数引用。例如文档中示例,将扩展函数所在文件 File.kt(包名为 pkg)中所有函数静态化:

    kotlin 复制代码
    mockkStatic("pkg.FileKt")
    every { Obj(5).extensionFunc() } returns 11

    如此可以模拟 Obj.extensionFunc() 的行为。总之,mockkStatic 适用于替换任何静态或顶层函数的实现,模拟完成后可用 unmockkStatic 解除

  • 对象(单例)的 mock: 对于 Kotlin 的 object 或 Java 的单例,可使用 mockkObject(SomeObject) 创建 mock 对象。此时可以像普通 mock 一样用 every { ... } 定义行为,并在测试后调用 unmockkObject(SomeObject) 恢复原状。

  • 注意事项: 使用 mockkStaticmockkObject 后要记得在测试结束时使用 unmockkStaticunmockkObject 清理,否则可能影响后续测试。

相关推荐
不想迷路的小男孩1 天前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
yzpyzp1 天前
Android studio在点击运行按钮时执行过程中输出的compileDebugKotlin 这个任务是由gradle执行的吗
android·gradle·android studio
新world1 天前
mybatis-plus从入门到入土(二):单元测试
单元测试·log4j·mybatis
花王江不语2 天前
android studio 配置硬件加速 haxm
android·ide·android studio
仰望星空@脚踏实地2 天前
Spring Boot Web 服务单元测试设计指南
spring boot·后端·单元测试
岁月玲珑3 天前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
小蜜蜂嗡嗡3 天前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
summer夏1234 天前
2025.07 做什么
java·android studio
啃火龙果的兔子4 天前
前端单元测试覆盖率工具有哪些,分别有什么优缺点
前端·单元测试
编程乐学14 天前
网络资源模板--基于Android Studio 实现的咖啡点餐App
android·android studio·大作业·奶茶点餐·安卓移动开发·咖啡点餐