web-dev-qa-db-ja.com

Kotlinシングルトンオブジェクトをモックする方法は?

Kotlinシングルトンオブジェクトと、そのメソッドを呼び出すfunが与えられた場合

_object SomeObject {
   fun someFun() {}
}

fun callerFun() {
   SomeObject.someFun()
}
_

SomeObject.someFun()の呼び出しを模擬する方法はありますか?

21
user3284037

オブジェクトをインターフェイスに実装するだけで、モックライブラリでオブジェクトをモックできます。 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)
    }
}

または、SomeObjectの中にcallerFunをモックする場合:

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)
    }
}
12
Ruslan

Kotlin用の非常に素晴らしいモックライブラリがあります- Mockk 。これは、オブジェクトをモックすることができます。

ドキュメントの時点で:


オブジェクトは次の方法でモックに変換できます。

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

元に戻すには、unmockkAllまたは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)
}

Kotlin言語の制限にもかかわらず、テストロジックで必要な場合は、オブジェクトの新しいインスタンスを作成できます。

val newObjectMock = mockk<MockObj>()
15
Kerooker

クラス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が初期化された後、デリゲートはすべての機能を処理します。
詳細については、公式の ドキュメント をご覧ください。

7
Ioane Sharvadze

非常に便利な Mockk ライブラリを使用するほかに、Mockitoとリフレクションを使用してobjectを単純にモックできます。 Kotlinオブジェクトは、プライベートコンストラクターとINSTANCE静的フィールドを持つ通常のJavaクラスです。リフレクションを使用すると、INSTANCEの値をモック付きに置き換えることができますオブジェクト。テスト後、変更が他のテストに影響を与えないように、オリジナルを復元する必要があります

Mockito Kotlinの使用(説明されているように拡張構成を追加する必要があります here 模擬最終クラスに):

testCompile "com.nhaarman:mockito-kotlin:1.5.0"

最初の楽しみは、INSTANCEクラスのstatic objectフィールドの値を置き換え、前の値を返すことです。

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のモックインスタンスを作成し、元の値をモックされた値で置き換え、後でリセットできるように元の値を返すという楽しみがあります。

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

コトリンシュガーを加える

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(""))
4
lelloman

バイトコードを操作するのではなく、コードを変更する意思がなければ、答えはノーです。 callerFunSomeObject.someFun()への呼び出しをモックする最も簡単な方法(および私が推奨する方法)は、モックオブジェクトをスリップする方法を提供することです。

例えば.

object SomeObject {
    fun someFun() {}
}

fun callerFun() {
    _callerFun { SomeObject.someFun() }
}

internal inline fun _callerFun(caller: () -> Unit) {
    caller()
}

ここでのアイデアは、あなたが変えたいと思うものを変えることです。シングルトンとそのシングルトンに作用するトップレベル関数が必要な場合は、上に示したように、パブリック署名を変更せずにトップレベル関数をテスト可能にする方法は、実装をinternal関数に移動することですモックを滑らせることができます。

1
mfulton26