如何模拟 Kotlin 中的对象?

How to mock objects in Kotlin?

我想测试调用对象的 class(java 中的静态方法调用),但我无法模拟此对象以避免执行真正的方法。

object Foo {
    fun bar() {
        //Calls third party sdk here
    }
}

我尝试了不同的选项,例如 Mockk, 并以与 java 相同的方式使用 PowerMock,但没有成功。

使用 PowerMockito 的代码:

@RunWith(PowerMockRunner::class)
@PrepareForTest(IntentGenerator::class)
class EditProfilePresenterTest {

    @Test
    fun shouldCallIntentGenerator() {

        val intent = mock(Intent::class.java)

        PowerMockito.mockStatic(IntentGenerator::class.java)
        PowerMockito.`when`(IntentGenerator.newIntent(any())).thenReturn(intent) //newIntent method param is context

       presenter.onGoToProfile()

       verify(view).startActivity(eq(intent))        

    }
}

使用此代码我得到

java.lang.IllegalArgumentException: Parameter specified as non-null is null: method com.sample.test.IntentGenerator$Companion.newIntent, parameter context

any() 方法来自 mockito_kotlin。然后,如果我将模拟上下文传递给 newIntent 方法,则似乎调用了真正的方法。

This blog post 清楚地演示了如何使用 mockito 执行此操作。我强烈推荐这种方法。真的很简单。

基本上,在您的 src/test/resources 文件夹中创建一个文件夹:mockito-extensions。在该目录中添加一个名为 org.mockito.plugins.MockMaker 的文件,该文件中包含以下文本:

mock-maker-inline

您现在可以模拟最终的 kotlin 类 和方法。

检查你的 any(),它 returns 为空,这就是你的异常的原因。

要调试,请将 any() 替换为 any().also(::println) 并查看命令行输出。
如果 any().also(::println) 失败请使用 any<MockContext>().also(::println).

我刚刚阅读了模拟框架的实现。 any 有一个隐式推断的类型参数。从你的错误信息来看,我猜 newIntent 的参数类型是 Context,这是一个抽象的 class,所以你当然不能创建它的实例。

如果我的猜测是正确的,将 any() 替换为 any<Activity>() 可能会解决这个问题。
不管我的猜测是否正确,请阅读 Whosebug 帮助中心和 "git gud scrub" 如何提出编程问题。您提供的信息对解决您的问题毫无帮助。

首先,object IntentGenerator 看起来有点代码味,你为​​什么要把它变成 object?如果这不是您的代码,您可以轻松地创建一个包装器 class

class IntentGeneratorWrapper {

    fun newIntent(context: Context) = IntentGenerator.newIntent(context)    

}

并在您的代码中使用它,没有静态依赖性。

也就是说,我有 2 个解决方案。假设你有一个 object

object IntentGenerator {
    fun newIntent(context: Context) = Intent()
}

解决方案 1 - Mockk

使用 Mockk 库,与 Mockito 相比,语法有点有趣,但是,嘿,它有效:

testCompile "io.mockk:mockk:1.7.10"
testCompile "com.nhaarman:mockito-kotlin:1.5.0"

然后在你的测试中你使用 objectMockk fun 和你的 object 作为参数,这将 return 你调用 use 的范围,在 [=21] =] body 你可以模拟 object:

@Test
fun testWithMockk() {
    val intent: Intent = mock()
    whenever(intent.action).thenReturn("meow")

    objectMockk(IntentGenerator).use {
        every { IntentGenerator.newIntent(any()) } returns intent
        Assert.assertEquals("meow", IntentGenerator.newIntent(mock()).action)
    }
}

解决方案 2 - Mockito + 反射

在您的测试资源文件夹中创建一个 mockito-extensions 文件夹(例如,如果您的模块是 "app" -> app/src/test/resources/mockito-extensions)并在其中创建一个名为 org.mockito.plugins.MockMaker 的文件.在文件中只写这一行mock-maker-inline。现在你可以模拟最终的 classes 和方法(IntentGenerator class 和 newIntent 方法都是最终的)。

然后你需要

  1. 创建 IntentGenerator 的实例。请注意 IntentGenerator 只是一个普通的 java class,我邀请您在 Android Studio
  2. 中使用 Kotlin bytecode window 进行检查]
  3. 在该实例上使用 Mockito 创建一个间谍对象并模拟该方法
  4. INSTANCE 字段中删除最终修饰符。当您在 Kotlin 中声明一个 object 时,会发生一个 class(在本例中为 IntentGenerator)是使用私有构造函数和静态 INSTANCE 方法创建的。即单例。
  5. 用您自己的模拟实例替换 IntentGenerator.INSTANCE 值。

完整的方法如下所示:

@Test
fun testWithReflection() {
    val intent: Intent = mock()
    whenever(intent.action).thenReturn("meow")

    // instantiate IntentGenerator
    val constructor = IntentGenerator::class.java.declaredConstructors[0]
    constructor.isAccessible = true
    val intentGeneratorInstance = constructor.newInstance() as IntentGenerator

    // mock the the method
    val mockedInstance = spy(intentGeneratorInstance)
    doAnswer { intent }.`when`(mockedInstance).newIntent(any())

    // remove the final modifier from INSTANCE field
    val instanceField = IntentGenerator::class.java.getDeclaredField("INSTANCE")
    val modifiersField = Field::class.java.getDeclaredField("modifiers")
    modifiersField.isAccessible = true
    modifiersField.setInt(instanceField, instanceField.modifiers and Modifier.FINAL.inv())

    // set your own mocked IntentGenerator instance to the static INSTANCE field
    instanceField.isAccessible = true
    instanceField.set(null, mockedInstance)

    // and BAM, now IntentGenerator.newIntent() is mocked
    Assert.assertEquals("meow", IntentGenerator.newIntent(mock()).action)
}

问题是在你mock对象之后,mocked的实例会留在那里,其他测试可能会受到影响。 A 制作了一个关于如何将模拟限制在一个范围内的示例

为什么 PowerMock 不工作

你得到

Parameter specified as non-null is null

因为 IntentGenerator 没有被模拟,因此被调用的方法 newIntent 是实际的方法,在 Kotlin 中,带有 non-null 参数的方法将调用 kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull 在你的方法的开头。您可以使用 Android Studio 中的字节码查看器进行检查。如果您将代码更改为

PowerMockito.mockStatic(IntentGenerator::class.java)
PowerMockito.doAnswer { intent }.`when`(IntentGenerator).newIntent(any())

你会得到另一个错误

org.mockito.exceptions.misusing.NotAMockException: Argument passed to when() is not a mock!

如果对象被模拟,调用的 newInstance 方法将不是来自实际 class 的方法,因此 null 可以作为参数传递,即使在签名中也是如此non-null可以