如何模拟 Kotlin 单例对象?
How to mock a Kotlin singleton object?
给定一个 Kotlin 单例对象和一个调用它的方法的 fun
object SomeObject {
fun someFun() {}
}
fun callerFun() {
SomeObject.someFun()
}
有没有办法模拟调用 SomeObject.someFun()
?
除了操纵字节码之外,答案是否定的,除非您愿意并且能够更改代码。模拟 callerFun
对 SomeObject.someFun()
的调用的最直接方法(也是我推荐的方法)是提供一些方法来将其滑动为模拟对象。
例如
object SomeObject {
fun someFun() {}
}
fun callerFun() {
_callerFun { SomeObject.someFun() }
}
internal inline fun _callerFun(caller: () -> Unit) {
caller()
}
这里的想法是改变你愿意改变的东西。如果你确定你想要一个单例和一个作用于该单例的顶级函数,那么一种方法,如上所示,使顶级函数可测试而不改变其 public 签名是移动它的实现到允许滑动模拟的 internal
函数。
只要让你的对象实现一个接口,你就可以用任何模拟库模拟你的对象。这里是 Junit + Mockito + Mockito-Kotlin:
的例子
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Assert.assertEquals
import org.junit.Test
object SomeObject : SomeInterface {
override fun someFun():String {
return ""
}
}
interface SomeInterface {
fun someFun():String
}
class SampleTest {
@Test
fun test_with_mock() {
val mock = mock<SomeInterface>()
whenever(mock.someFun()).thenReturn("42")
val answer = mock.someFun()
assertEquals("42", answer)
}
}
或者如果你想在 callerFun
中模拟 SomeObject
:
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Assert.assertEquals
import org.junit.Test
object SomeObject : SomeInterface {
override fun someFun():String {
return ""
}
}
class Caller(val someInterface: SomeInterface) {
fun callerFun():String {
return "Test ${someInterface.someFun()}"
}
}
// Example of use
val test = Caller(SomeObject).callerFun()
interface SomeInterface {
fun someFun():String
}
class SampleTest {
@Test
fun test_with_mock() {
val mock = mock<SomeInterface>()
val caller = Caller(mock)
whenever(mock.someFun()).thenReturn("42")
val answer = caller.callerFun()
assertEquals("Test 42", answer)
}
}
有一个非常好的 Kotlin 模拟库 - Mockk,它允许您模拟对象,就像您想要的一样。
根据其文档:
Objects can be transformed to mocks following way:
object MockObj {
fun add(a: Int, b: Int) = a + b
}
mockkObject(MockObj) // aplies mocking to an Object
assertEquals(3, MockObj.add(1, 2))
every { MockObj.add(1, 2) } returns 55
assertEquals(55, MockObj.add(1, 2))
To revert back use unmockkAll or unmockkObject:
@Before
fun beforeTests() {
mockkObject(MockObj)
every { MockObj.add(1,2) } returns 55
}
@Test
fun willUseMockBehaviour() {
assertEquals(55, MockObj.add(1,2))
}
@After
fun afterTests() {
unmockkAll()
// or unmockkObject(MockObj)
}
Despite Kotlin language limits you can create new instances of objects if testing logic needs that:
val newObjectMock = mockk<MockObj>()
通过使用 class delegates,您可以在没有任何额外库的情况下模拟对象。
这是我的提议
val someObjectDelegate : SomeInterface? = null
object SomeObject: by someObjectDelegate ?: SomeObjectImpl
object SomeObjectImpl : SomeInterface {
fun someFun() {
println("SomeObjectImpl someFun called")
}
}
interface SomeInterface {
fun someFun()
}
在您的测试中,您可以设置将改变行为的委托对象,否则它将使用它的真实实现。
@Beofre
fun setUp() {
someObjectDelegate = object : SomeInterface {
fun someFun() {
println("Mocked function")
}
}
// Will call method from your delegate
SomeObject.someFun()
}
当然上面的名字不好,但是为了举例说明目的。
SomeObject 初始化后委托将处理所有函数。
更多你可以在官方找到 documentation
除了使用非常方便的 Mockk 库之外,还可以简单地使用 Mockito 和反射来模拟 object
。一个 Kotlin 对象只是一个带有私有构造函数和一个 INSTANCE
静态字段的常规 Java class,通过反射可以用模拟对象替换 INSTANCE
的值。测试后应恢复原件,这样更改不会影响其他测试
使用 Mockito Kotlin(需要添加扩展配置 here 以模拟最终 classes):
testCompile "com.nhaarman:mockito-kotlin:1.5.0"
第一个乐趣可以替换 object
中静态 INSTANCE
字段的值 class 和 return 以前的值
fun <T> replaceObjectInstance(clazz: Class<T>, newInstance: T): T {
if (!clazz.declaredFields.any {
it.name == "INSTANCE" && it.type == clazz && Modifier.isStatic(it.modifiers)
}) {
throw InstantiationException("clazz ${clazz.canonicalName} does not have a static " +
"INSTANCE field, is it really a Kotlin \"object\"?")
}
val instanceField = clazz.getDeclaredField("INSTANCE")
val modifiersField = Field::class.java.getDeclaredField("modifiers")
modifiersField.isAccessible = true
modifiersField.setInt(instanceField, instanceField.modifiers and Modifier.FINAL.inv())
instanceField.isAccessible = true
val originalInstance = instanceField.get(null) as T
instanceField.set(null, newInstance)
return originalInstance
}
然后你可以玩得开心,创建一个 object
的模拟实例,并用模拟的实例替换原始值,returning 原始值,以便以后可以重置它
fun <T> mockObject(clazz: Class<T>): T {
val constructor = clazz.declaredConstructors.find { it.parameterCount == 0 }
?: throw InstantiationException("class ${clazz.canonicalName} has no empty constructor, " +
"is it really a Kotlin \"object\"?")
constructor.isAccessible = true
val mockedInstance = spy(constructor.newInstance() as T)
return replaceObjectInstance(clazz, mockedInstance)
}
添加一些 Kotlin 糖
class MockedScope<T : Any>(private val clazz: Class<T>) {
fun test(block: () -> Unit) {
val originalInstance = mockObject(clazz)
block.invoke()
replaceObjectInstance(clazz, originalInstance)
}
}
fun <T : Any> withMockObject(clazz: Class<T>) = MockedScope(clazz)
最后,给定一个 object
object Foo {
fun bar(arg: String) = 0
}
你可以这样测试
withMockObject(Foo.javaClass).test {
doAnswer { 1 }.whenever(Foo).bar(any())
Assert.assertEquals(1, Foo.bar(""))
}
Assert.assertEquals(0, Foo.bar(""))
对于 Mockito,我们可以使用 Mockito.mockStatic()
Mockito.mockStatic(SomeObject::class.java).use { mocked ->
mocked.`when`<SomeType> { SomeObject.callAFunction() }
.thenReturn(someMockedValue)
// Your test goes here
}
在use
的范围之外,callAFunction()
的值被重置
给定一个 Kotlin 单例对象和一个调用它的方法的 fun
object SomeObject {
fun someFun() {}
}
fun callerFun() {
SomeObject.someFun()
}
有没有办法模拟调用 SomeObject.someFun()
?
除了操纵字节码之外,答案是否定的,除非您愿意并且能够更改代码。模拟 callerFun
对 SomeObject.someFun()
的调用的最直接方法(也是我推荐的方法)是提供一些方法来将其滑动为模拟对象。
例如
object SomeObject {
fun someFun() {}
}
fun callerFun() {
_callerFun { SomeObject.someFun() }
}
internal inline fun _callerFun(caller: () -> Unit) {
caller()
}
这里的想法是改变你愿意改变的东西。如果你确定你想要一个单例和一个作用于该单例的顶级函数,那么一种方法,如上所示,使顶级函数可测试而不改变其 public 签名是移动它的实现到允许滑动模拟的 internal
函数。
只要让你的对象实现一个接口,你就可以用任何模拟库模拟你的对象。这里是 Junit + Mockito + Mockito-Kotlin:
的例子import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Assert.assertEquals
import org.junit.Test
object SomeObject : SomeInterface {
override fun someFun():String {
return ""
}
}
interface SomeInterface {
fun someFun():String
}
class SampleTest {
@Test
fun test_with_mock() {
val mock = mock<SomeInterface>()
whenever(mock.someFun()).thenReturn("42")
val answer = mock.someFun()
assertEquals("42", answer)
}
}
或者如果你想在 callerFun
中模拟 SomeObject
:
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever
import org.junit.Assert.assertEquals
import org.junit.Test
object SomeObject : SomeInterface {
override fun someFun():String {
return ""
}
}
class Caller(val someInterface: SomeInterface) {
fun callerFun():String {
return "Test ${someInterface.someFun()}"
}
}
// Example of use
val test = Caller(SomeObject).callerFun()
interface SomeInterface {
fun someFun():String
}
class SampleTest {
@Test
fun test_with_mock() {
val mock = mock<SomeInterface>()
val caller = Caller(mock)
whenever(mock.someFun()).thenReturn("42")
val answer = caller.callerFun()
assertEquals("Test 42", answer)
}
}
有一个非常好的 Kotlin 模拟库 - Mockk,它允许您模拟对象,就像您想要的一样。
根据其文档:
Objects can be transformed to mocks following way:
object MockObj {
fun add(a: Int, b: Int) = a + b
}
mockkObject(MockObj) // aplies mocking to an Object
assertEquals(3, MockObj.add(1, 2))
every { MockObj.add(1, 2) } returns 55
assertEquals(55, MockObj.add(1, 2))
To revert back use unmockkAll or unmockkObject:
@Before
fun beforeTests() {
mockkObject(MockObj)
every { MockObj.add(1,2) } returns 55
}
@Test
fun willUseMockBehaviour() {
assertEquals(55, MockObj.add(1,2))
}
@After
fun afterTests() {
unmockkAll()
// or unmockkObject(MockObj)
}
Despite Kotlin language limits you can create new instances of objects if testing logic needs that:
val newObjectMock = mockk<MockObj>()
通过使用 class delegates,您可以在没有任何额外库的情况下模拟对象。
这是我的提议
val someObjectDelegate : SomeInterface? = null
object SomeObject: by someObjectDelegate ?: SomeObjectImpl
object SomeObjectImpl : SomeInterface {
fun someFun() {
println("SomeObjectImpl someFun called")
}
}
interface SomeInterface {
fun someFun()
}
在您的测试中,您可以设置将改变行为的委托对象,否则它将使用它的真实实现。
@Beofre
fun setUp() {
someObjectDelegate = object : SomeInterface {
fun someFun() {
println("Mocked function")
}
}
// Will call method from your delegate
SomeObject.someFun()
}
当然上面的名字不好,但是为了举例说明目的。
SomeObject 初始化后委托将处理所有函数。
更多你可以在官方找到 documentation
除了使用非常方便的 Mockk 库之外,还可以简单地使用 Mockito 和反射来模拟 object
。一个 Kotlin 对象只是一个带有私有构造函数和一个 INSTANCE
静态字段的常规 Java class,通过反射可以用模拟对象替换 INSTANCE
的值。测试后应恢复原件,这样更改不会影响其他测试
使用 Mockito Kotlin(需要添加扩展配置 here 以模拟最终 classes):
testCompile "com.nhaarman:mockito-kotlin:1.5.0"
第一个乐趣可以替换 object
中静态 INSTANCE
字段的值 class 和 return 以前的值
fun <T> replaceObjectInstance(clazz: Class<T>, newInstance: T): T {
if (!clazz.declaredFields.any {
it.name == "INSTANCE" && it.type == clazz && Modifier.isStatic(it.modifiers)
}) {
throw InstantiationException("clazz ${clazz.canonicalName} does not have a static " +
"INSTANCE field, is it really a Kotlin \"object\"?")
}
val instanceField = clazz.getDeclaredField("INSTANCE")
val modifiersField = Field::class.java.getDeclaredField("modifiers")
modifiersField.isAccessible = true
modifiersField.setInt(instanceField, instanceField.modifiers and Modifier.FINAL.inv())
instanceField.isAccessible = true
val originalInstance = instanceField.get(null) as T
instanceField.set(null, newInstance)
return originalInstance
}
然后你可以玩得开心,创建一个 object
的模拟实例,并用模拟的实例替换原始值,returning 原始值,以便以后可以重置它
fun <T> mockObject(clazz: Class<T>): T {
val constructor = clazz.declaredConstructors.find { it.parameterCount == 0 }
?: throw InstantiationException("class ${clazz.canonicalName} has no empty constructor, " +
"is it really a Kotlin \"object\"?")
constructor.isAccessible = true
val mockedInstance = spy(constructor.newInstance() as T)
return replaceObjectInstance(clazz, mockedInstance)
}
添加一些 Kotlin 糖
class MockedScope<T : Any>(private val clazz: Class<T>) {
fun test(block: () -> Unit) {
val originalInstance = mockObject(clazz)
block.invoke()
replaceObjectInstance(clazz, originalInstance)
}
}
fun <T : Any> withMockObject(clazz: Class<T>) = MockedScope(clazz)
最后,给定一个 object
object Foo {
fun bar(arg: String) = 0
}
你可以这样测试
withMockObject(Foo.javaClass).test {
doAnswer { 1 }.whenever(Foo).bar(any())
Assert.assertEquals(1, Foo.bar(""))
}
Assert.assertEquals(0, Foo.bar(""))
对于 Mockito,我们可以使用 Mockito.mockStatic()
Mockito.mockStatic(SomeObject::class.java).use { mocked ->
mocked.`when`<SomeType> { SomeObject.callAFunction() }
.thenReturn(someMockedValue)
// Your test goes here
}
在use
的范围之外,callAFunction()
的值被重置