IP地址检测
视频播放器
输入关键词搜索
登录
Android单元测试

Android单元测试

2025-05-15
暂无分类

通用

assert:用于条件验证,verify:验证方法是否被调用、调用次数、顺序、参数等

answers:mock函数体,coAnswers:mock协程函数体

returns:mock函数的返回值,returnsMany:函数多次调用时按顺序返回多个不同值

throws:mock异常

mockk<T>():创建接口/类的 mock 实例

class Person() {
    var name: String = ""
    var age: Int = 0
}

// 1. relaxed = true 代表所有属性用默认值。否则,每个属性你都要手动mock。如果访问未mock的属性或函数时,会抛异常
val person = mockk<Person>(relaxed = true)
// 2. mock时自定义属性的值
val person = mockk<Person>(relaxed = true)
every { person.name } returns "hello"
every { person.age } returns 18
// 3. 也可以用下面这种写法
val person = mockk<Person>(relaxed = true) {
    every { name } returns "hello"
    every { age } returns 18
}

mockkObject:用于 Kotlin object 单例

假设我们有这样一个对象:

// File: Utils.kt
object Utils {
    fun getCurrentTime(): Long = System.currentTimeMillis()
    fun greet(name: String): String = "Hello, $name"
}

单元测试中 mock 它:

import io.mockk.*
import kotlin.test.*

class UtilsTest {

    @BeforeTest
    fun setUp() {
        mockkObject(Utils) // ✅ mock 单例对象
    }

    @AfterTest
    fun tearDown() {
        unmockkObject(Utils) // ✅ 清理 mock,防止污染其他测试
    }

    @Test
    fun testMockObjectFunction() {
        every { Utils.getCurrentTime() } returns 123456789L
        every { Utils.greet("Alice") } returns "Hi, Alice"

        assertEquals(123456789L, Utils.getCurrentTime())
        assertEquals("Hi, Alice", Utils.greet("Alice"))

        verify {
            Utils.getCurrentTime()
            Utils.greet("Alice")
        }
    }
}

mockkStatic:针对顶层/静态方法进行 mock

kt文件-顶层函数

如果你要 mock 的是顶层函数,最简洁的方式是直接传入函数引用

假设在 com.example.HelloWorld.kt 文件中有:

package com.example

fun sayHello(): String = "Hello Kotlin!"

mock方式:

import com.example.sayHello   // 导入顶层函数
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Test
import kotlin.test.assertEquals

class HelloWorldTest {
  @Test
  fun `mock sayHello via function reference`() {
    // 使用函数引用
    mockkStatic(::sayHello)
    every { sayHello() } returns "Mocked Hello"

    assertEquals("Mocked Hello", sayHello())
  }
}

mockkStatic(::sayHello)会把顶层函数sayHello变为可mock的静态方法

后续再对同名函数的调用,均走 stub 逻辑,而不执行原始实现。

kt文件-顶层扩展函数

在 com/example/StringExt.kt 文件中, 定义了一个String的扩展函数

package com.example

fun String.starSurround(): String = "*$this*"

要 mock 顶层扩展函数,你不能这么写:

mockkStatic(::starSurround) // ❌ 不行!

正确写法:

mockkStatic("com.example.StringExtKt") // 传入编译后生成的 Java 类名(文件名 + Kt)
every { any<String>().starSurround() } returns "#hello#"

assertEquals("hello".startSurround(), "#hello#")

问题排查记录:

1.mock没有成功,要判断是不是代码中用到的对象和单元测试中的mock对象是否是同一个

遇到的坑!!!

坑一:多次调用 mockkStatic(...) 同一类,后写的会覆盖前面写的

// 工具类 Util.kt,定义了两个顶层函数

fun getProductList():{}
fun getTitle():{}

// 代码示例

mockkStatic("com.example.app.UtilKt")
every { Util.getProductList() } answers { println("被mock了") }
mockkStatic(::getTitle) // ❌ 这个mock函数会覆盖上面的mock,导致上面的mock失效
every { getTitle() } answers { println("被mock了") }

✅正确的写法是:如果一个类中有多个函数需要mock,应该用“包名”的写法

mockkStatic("com.example.app.UtilKt")
every { Util.getProductList() } answers { println("被mock了") }
every { Util.getTitle() } answers { println("被mock了") }

坑2:public属性应该直接赋值,不能用every方法mock

MockK的主要功能是拦截和控制模拟对象的方法调用以及属性的访问(即 getter 和 setter 的行为)。MockK 并没有为公共字段(public)的行为添加额外的拦截逻辑,而是保留了直接操作字段的能力

如果我们尝试用 MockK 的 every 语法来模拟公共字段,比如:

mockk<PackageInfo>(relaxed = true) {
    every { packageName } returns appPkgName
}

会抛出异常(例如 “Missing mocked calls inside every { ... } block”)。这是因为 every 是设计用来模拟方法调用或属性 getter/setter 行为的,而 packageName 是一个字段,不是方法或属性。MockK 无法拦截对字段的直接访问,因此这种写法是无效的。

✅正确的写法应该直接赋值:

mockk<PackageInfo>(relaxed = true) {
    packageName = appPkgName
}

坑3:TextUtils.equals("string","string"),即使一样也返回false

原因:在普通的 JVM 单元测试(runTest)里,你并不是真在 Android 设备/模拟器上跑代码,而是在本地用 Android SDK 里那个“空壳” android.jar(只包含接口和方法签名,不包含真正实现)来编译和运行。也就是说,android.text.TextUtils.equals(...) 在本地测试时是个空方法,返回的永远都是它的默认值——false。

直接用 == 运算符,它在幕后会调用 equals() 做“结构相等”(equals())比较,并且对 null 安全:

assertEquals("aaa","aaa")

坑4:Android 自带的 org.json 在 JVM 测试里并不一定可用

// 这段代码打印 null
@Test
fun test() = runTest {
    val jsonObject = JSONObject()
    println(jsonObject)
}

Android 的 org.json 并不是标准 Java 的实现,而是 Android SDK 内部提供的一个版本。这时,如果依赖 org.json,实际用到的是 stub(空实现),导致 JSONObject 等不可用,表现为构造对象返回null、方法调用无实际功能,甚至抛异常。

解决办法

办法一:在 build.gradle 文件中为 testImplementation 增加 org.json 的依赖。版本号可在maven仓库查看

testImplementation 'org.json:json:20250517'

办法二:或者,将相关测试迁移到 androidTest 目录

THE END
0/500
暂无评论