news 2026/3/22 14:47:49

Android单元测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android单元测试

Android单元测试基础

单元测试用于验证应用中最小单元(函数或类)的行为是否正确。在 Android/Kotlin 项目中,本地单元测试通常放在module/src/test/目录下,使用 JUnit4 框架编写。要启用测试,需要在 Gradle 中添加依赖,例如

testImplementation "junit:junit:版本号"(JUnit4)和testImplementation "io.mockk:mockk:版本号"(MockK)。

每个测试类中包含一个或多个用@Test注解标记的方法,在方法体内调用被测函数并使用断言验证输出。

  1. class EmailValidatorTest {

  2. @Test

  3. fun emailValidator_CorrectEmailSimple_ReturnsTrue() {

  4. assertTrue(EmailValidator.isValidEmail("name@example.com"))

  5. }

  6. }

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

测试替身

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

依赖隔离,核心逻辑:

关键概念解析
替身类型作用场景示例
Mock验证交互行为检查是否调用了数据库API
Stub返回预设数据固定返回用户{id:1, name:测试}
Fake简化实现替代真实服务内存数据库替代MySQL
Spy记录调用信息(Mock的变体)记录网络请求次数
Dummy填充参数(无逻辑)new Object()占位
在Android测试中的典型应用
  1. // 使用Mockito框架示例

  2. @Mock

  3. Database mockDB; // 创建数据库Mock

  4. @Test

  5. public void testUserSave() {

  6. // 1. 设置Stub行为

  7. when(mockDB.save(any(User.class))).thenReturn(true);

  8. // 2. 执行被测方法

  9. service.saveUser(new User("test"));

  10. // 3. 验证Mock交互

  11. verify(mockDB).save(any(User.class));

  12. }

核心价值
  • 🛡️隔离性:避免测试因网络/DB故障而失败
  • 加速测试:移除真实IO操作(原需200ms→2ms)
  • 🔍行为验证:确保调用次数/参数符合预期
  • 🧪边界覆盖:轻松模拟异常场景(如:when(...).thenThrow(...)

MockK 概念与常用用法

MockK 是一个专为 Kotlin 设计的模拟测试框架,相比 Mockito 等 Java 库,MockK 自然支持 Kotlin 的特性,如 final 类、扩展函数和协程。使用 MockK 可以方便地创建接口或类的 mock 对象,并通过 DSL 定义其行为。最简单的使用方法如下:

  1. val car = mockk<Car>() // 创建 Car 类的 mock 对象

  2. every { car.drive(Direction.NORTH) } returns Outcome.OK // 定义方法返回值

  3. car.drive(Direction.NORTH) // 调用时返回 OK

  4. verify { car.drive(Direction.NORTH) } // 验证方法被调用

  5. confirmVerified(car)

以上示例来自 MockK 官方文档。其中mockk<T>()会创建一个严格模式(strict)的 mock 对象,需要显式定义其所有行为(使用every { ... } returns ...)。

什么叫做严格模式
1.什么是严格模式?
  • 在 MockK 中,严格模式(也称为标准模式)意味着 mock 对象会严格执行以下规则:
    • 所有对 mock 对象方法的调用都必须预先声明(即使用every块定义行为)。
    • 如果调用了一个没有预先声明的方法,MockK 会立即抛出异常(MockKException: no answer found)。

例如:

  1. val car = mockk<Car>() // 创建严格模式的 mock 对象

  2. // 未定义行为时调用方法 → 抛出异常!

  3. car.drive(50) // 抛出 no answer found 异常

2.显式定义行为

使用every { ... } returns ...结构为 mock 对象的方法定义行为:

every { car.drive(50) } returns "Driving at 50 km/h"
  • every:声明一个预期调用的行为。
  • returns:指定该方法调用的返回值。

此时调用car.drive(50)会返回"Driving at 50 km/h"

3.为何需要显式定义所有行为?
  • 避免隐藏错误:严格模式强制测试编写者明确指定 mock 对象的所有预期行为。这有助于暴露测试中的隐含假设或遗漏的依赖调用。
  • 提高测试可靠性:确保测试只关注预先定义的行为,避免因意外调用导致的假阳性/假阴性结果。
4.未定义行为的后果

如果在严格模式下调用未定义的方法,会收到如下错误:

io.mockk.MockKException: no answer found for: Car(#1).drive(50)
5.对比:严格模式 vs 松弛模式
模式是否需要显式定义行为未定义行为时的处理
严格模式抛出异常
松弛模式返回默认值(null, 0 等)

松弛模式的创建方式:

val relaxedCar = mockk<Car>(relaxed = true) // 不会因未定义行为抛出异常
6.何时使用严格模式?
  • 推荐场景
    1. 需要精确控制 mock 行为的测试。
    1. 验证代码是否按预期调用了特定方法(通常结合verify)。
  • 不推荐场景:当 mock 对象有许多不重要的方法(如纯数据模型),使用严格模式会写大量every块,此时可改用松弛模式。
7.完整示例
  1. // 定义类

  2. class Car {

  3. fun drive(speed: Int): String = "Real driving: $speed km/h"

  4. }

  5. // 测试

  6. @Test

  7. fun testStrictMock() {

  8. // 1. 创建严格模式 mock

  9. val car = mockk<Car>()

  10. // 2. 显式定义行为

  11. every { car.drive(50) } returns "Mocked driving at 50 km/h"

  12. // 3. 调用已定义方法 → 成功

  13. assertEquals("Mocked driving at 50 km/h", car.drive(50))

  14. // 4. 调用未定义方法 → 抛出异常!

  15. assertFailsWith<MockKException> {

  16. car.drive(100) // 未定义 100 的行为

  17. }

  18. }

总结
  • mockk<T>()创建的是严格模式的 mock 对象。
  • 必须every { ... } returns ...为其每个需要调用的方法定义行为。
  • 严格模式能提高测试的精确性,但会增加样板代码。根据场景选择是否使用松弛模式(relaxed = true)。### 详细解释:mockk<T>()创建严格模式(Strict Mode)的 Mock 对象
核心概念:严格模式 (Strict Mode)
val myService = mockk<MyService>() // 创建严格模式的 mock 对象
  • 行为必须显式定义

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

  1. // 必须为每个需要调用的方法定义行为

  2. every { myService.getData() } returns "MockedData"

  3. every { myService.calculate(any()) } returns 42

为什么需要严格模式?
场景严格模式非严格模式
未定义方法调用立即抛出异常返回默认值(null, 0 等)
测试可靠性确保不会意外调用未模拟的方法可能隐藏未覆盖的依赖
测试意图明确声明所有预期行为隐含接受默认行为

典型错误示例
  1. // 测试代码

  2. val userService = mockk<UserService>() // 严格模式

  3. // ❌ 忘记定义行为

  4. userService.findUser("id123") // 抛出 MockKException!

  5. // ✅ 正确做法

  6. every { userService.findUser(any()) } returns User("MockedUser")

  7. val result = userService.findUser("id123") // 返回 User 对象

严格模式的核心特点
  1. 零容忍未声明行为
    任何未通过every定义的调用都会导致测试失败。

  2. 精确控制模拟行为
    必须为每个参数组合指定行为:

    1. // 不同参数需要单独定义

    2. every { myService.parse("A") } returns 1

    3. every { myService.parse("B") } returns 2

    与验证的配合
    常与verify一起使用确保调用符合预期:

    1. every { myService.send(any()) } returns true

    2. // 测试代码

    3. myService.send("message")

    4. verify { myService.send("message") } // 验证调用发生

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

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

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:如果不想为每个方法都定义返回值,可以创建一个relaxedmock,即mockk<MyClass>(relaxed = true)。这会让所有非 Unit 返回类型的方法自动返回默认值(例如布尔型为 false,引用型为 null)。这样即使不显式 stub 方法,调用时也不会抛异常。需要注意,针对泛型返回类型的函数,放宽 mock 有时可能抛出类型转换异常。

  • 静态方法和顶层函数:Kotlin 的顶层函数或 Java 静态方法可以用mockkStatic()模拟。对于 Java 静态方法或类静态方法,直接传入类引用:

  1. mockkStatic(Uri::class)

  2. every { Uri.parse("http://test/path") } returns mockUri

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

  1. mockkStatic("pkg.FileKt")

  2. every { Obj(5).extensionFunc() } returns 11

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

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

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

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/21 9:16:54

Speech Seaco Paraformer省钱部署方案:按需GPU计费降低50%成本

Speech Seaco Paraformer省钱部署方案&#xff1a;按需GPU计费降低50%成本 1. 背景与痛点&#xff1a;语音识别落地为何总卡在成本上&#xff1f; 你是不是也遇到过这种情况&#xff1a;好不容易跑通了一个高精度的中文语音识别模型&#xff0c;结果一算账&#xff0c;每月GP…

作者头像 李华
网站建设 2026/3/21 12:54:23

Open-AutoGLM文档解读:核心模块与API接口使用指南

Open-AutoGLM文档解读&#xff1a;核心模块与API接口使用指南 1. 框架定位与能力全景 Open-AutoGLM 是智谱开源的轻量化手机端 AI Agent 框架&#xff0c;它不是传统意义上的大模型推理工具&#xff0c;而是一个真正能“看见”“理解”“动手”的多模态智能体系统。它的核心价…

作者头像 李华
网站建设 2026/3/16 5:17:09

MySQL错误1045排查全攻略(从用户权限到防火墙配置一网打尽)

第一章&#xff1a;PHP连接MySQL报错1045问题概述当使用PHP连接MySQL数据库时&#xff0c;开发人员常遇到错误代码1045&#xff0c;其完整提示通常为&#xff1a;Access denied for user usernamelocalhost (using password: YES)。该错误表明MySQL服务器拒绝了客户端的登录请求…

作者头像 李华
网站建设 2026/3/21 7:51:09

【工业级图像处理必备技能】:基于C++ OpenCV的多尺度模糊融合技术揭秘

第一章&#xff1a;多尺度模糊融合技术概述与工业应用场景 多尺度模糊融合技术是一种结合多分辨率分析与模糊逻辑推理的数据融合方法&#xff0c;广泛应用于复杂环境下的信号处理、图像增强和智能决策系统。该技术通过在不同尺度上提取输入数据的特征&#xff0c;并利用模糊规则…

作者头像 李华
网站建设 2026/3/15 14:23:34

R语言读取CSV文件中文乱码怎么办?3步快速解决,避免数据失真

第一章&#xff1a;R语言读取CSV文件中文乱码问题概述 在使用R语言处理数据时&#xff0c;读取包含中文字符的CSV文件常出现乱码问题。这一现象主要源于文件编码格式与R默认编码设置不匹配。例如&#xff0c;Windows系统下生成的CSV文件通常采用UTF-8或GBK编码&#xff0c;而R在…

作者头像 李华
网站建设 2026/3/21 10:33:30

unet person image cartoon compound更新日志解读:未来将支持日漫风

unet person image cartoon compound更新日志解读&#xff1a;未来将支持日漫风 1. 功能概述 本工具基于阿里达摩院 ModelScope 的 DCT-Net 模型&#xff0c;名为 unet person image cartoon compound&#xff0c;由开发者“科哥”构建并优化&#xff0c;专注于将真人照片智能…

作者头像 李华