如何在Kotlin中模拟物体?

alo*_*loj 3 android unit-testing mockito kotlin powermockito

我想测试一个调用一个对象的类(java中的静态方法调用),但是我无法模拟这个对象以避免执行真正的方法.

object Foo {
    fun bar() {
        //Calls third party sdk here
    }
}
Run Code Online (Sandbox Code Playgroud)

我尝试了不同的选项,如Mockk,如何模拟Kotlin单例对象?并以与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))        

    }
}
Run Code Online (Sandbox Code Playgroud)

有了这个代码,我得到了

java.lang.IllegalArgumentException:指定为非null的参数为null:方法com.sample.test.IntentGenerator $ Companion.newIntent,parameter context

any()方法来自mockito_kotlin.然后,如果我将一个模拟的上下文传递给newIntent方法,则看起来似乎是真正的方法.

lel*_*man 7

首先,这object IntentGenerator看起来像一个代码味道,为什么你会成为一个object?如果它不是您的代码,您可以轻松创建包装类

class IntentGeneratorWrapper {

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

}
Run Code Online (Sandbox Code Playgroud)

并在代码中使用那个,没有静态依赖.

话虽这么说,我有2个解决方案.说你有object

object IntentGenerator {
    fun newIntent(context: Context) = Intent()
}
Run Code Online (Sandbox Code Playgroud)

解决方案1 ​​ - Mockk

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

testCompile "io.mockk:mockk:1.7.10"
testCompile "com.nhaarman:mockito-kotlin:1.5.0"
Run Code Online (Sandbox Code Playgroud)

然后在你的测试中你使用objectMockk你的objectas参数的乐趣,并返回你调用的范围use,在use体内你可以模拟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)
    }
}
Run Code Online (Sandbox Code Playgroud)

解决方案2 - Mockito +反射

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

然后你需要

  1. 创建一个实例IntentGenerator.请注意,这IntentGenerator只是一个普通的java类,我邀请您Kotlin bytecode在Android Studio中使用窗口进行检查
  2. 在该实例上使用Mockito创建一个间谍对象并模拟该方法
  3. INSTANCE字段中删除最终修饰符.当您object在Kotlin中声明时,发生的事情是IntentGenerator使用私有构造函数和静态INSTANCE方法创建类(在本例中).也就是说,一个单身人士.
  4. 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)
}
Run Code Online (Sandbox Code Playgroud)

问题是,在模拟对象之后,模拟的实例将保留在那里,其他测试可能会受到影响.一个对如何限制嘲讽的一个样品放入一个范围在这里

为什么PowerMock不工作

你得到了

指定为非null的参数为null

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

PowerMockito.mockStatic(IntentGenerator::class.java)
PowerMockito.doAnswer { intent }.`when`(IntentGenerator).newIntent(any())
Run Code Online (Sandbox Code Playgroud)

你会得到另一个错误

org.mockito.exceptions.misusing.NotAMockException:传递给when()不是模拟的参数!

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