如何模拟 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
方法都是最终的)。
然后你需要
- 创建
IntentGenerator
的实例。请注意 IntentGenerator
只是一个普通的 java class,我邀请您在 Android Studio 中使用 Kotlin bytecode
window 进行检查]
- 在该实例上使用 Mockito 创建一个间谍对象并模拟该方法
- 从
INSTANCE
字段中删除最终修饰符。当您在 Kotlin 中声明一个 object
时,会发生一个 class(在本例中为 IntentGenerator
)是使用私有构造函数和静态 INSTANCE
方法创建的。即单例。
- 用您自己的模拟实例替换
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可以
我想测试调用对象的 class(java 中的静态方法调用),但我无法模拟此对象以避免执行真正的方法。
object Foo {
fun bar() {
//Calls third party sdk here
}
}
我尝试了不同的选项,例如 Mockk,
使用 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
方法都是最终的)。
然后你需要
- 创建
IntentGenerator
的实例。请注意IntentGenerator
只是一个普通的 java class,我邀请您在 Android Studio 中使用 - 在该实例上使用 Mockito 创建一个间谍对象并模拟该方法
- 从
INSTANCE
字段中删除最终修饰符。当您在 Kotlin 中声明一个object
时,会发生一个 class(在本例中为IntentGenerator
)是使用私有构造函数和静态INSTANCE
方法创建的。即单例。 - 用您自己的模拟实例替换
IntentGenerator.INSTANCE
值。
Kotlin bytecode
window 进行检查]
完整的方法如下所示:
@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可以