如何在Kotlin中模拟对象?

时间:2018-03-13 16:53:47

标签: android unit-testing kotlin mockito powermockito

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

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

我尝试过不同的选项,例如MockkHow to mock a Kotlin singleton object?和使用PowerMock的方式与java相同,但没有成功。

使用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:指定为非null的参数为null:方法com.sample.test.IntentGenerator $ Companion.newIntent,parameter context

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

3 个答案:

答案 0 :(得分:5)

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

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作为参数的object乐趣,这将返回您在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)
    }
}

解决方案2 - Mockito +反射

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

然后你需要

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

    @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)
    }
    

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

    中的示例

    为什么PowerMock无法正常工作

    你得到了

      

    指定为非null的参数为null

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

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

    你会收到另一个错误

      

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

    如果对象被模拟,则调用的newInstance方法不会是实际类中的方法,因此null可以作为参数传递,即使在签名中它是不可为空的< / p>

答案 1 :(得分:0)

检查你的any(),它返回null,这就是你的例外原因。

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

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

如果我的猜测属实,将any()替换为any<Activity>()可能会解决此问题 无论我的猜测是否正确,请阅读StackOverflow帮助中心和“git gud scrub”,了解如何提出编程问题。您提供的信息对于解决您的问题非常无益。

答案 2 :(得分:-2)

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

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

mock-maker-inline

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