web-dev-qa-db-fra.com

Comment se moquer d'un objet singleton Kotlin?

Étant donné un objet singleton Kotlin et un amusement qui appelle sa méthode

object SomeObject {
   fun someFun() {}
}

fun callerFun() {
   SomeObject.someFun()
}

Existe-t-il un moyen de simuler un appel à SomeObject.someFun()?

13
user3284037

Assurez-vous simplement que l'objet implémente une interface, que vous pouvez vous moquer de l'objet avec n'importe quelle bibliothèque moqueuse Voici un exemple de 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)
    }
}

Ou au cas où vous voudriez mimer SomeObject à l'intérieur de 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)
    }
}
6
IRus

Il existe une très jolie bibliothèque moqueuse pour Kotlin - Mockk , qui vous permet de vous moquer des objets, exactement comme vous le souhaitez.

Dès sa documentation:


Les objets peuvent être transformés en imitations de manière suivante:

object MockObj {
  fun add(a: Int, b: Int) = a + b
}

mockkObject(MockObj) {
  assertEquals(3, MockObj.add(1, 2))

  every { MockObj.add(1, 2) } returns 55

  assertEquals(55, MockObj.add(1, 2))
}

Malgré les limites du langage Kotlin, vous pouvez créer de nouvelles instances d’objets si la logique de test nécessite:

val newObjectMock = mockk<MockObj>()
7
Kerooker

Vous pouvez vous moquer d’Object sans bibliothèque supplémentaire en utilisant class délégués

Voici ma proposition

val someObjectDelegate : SomeInterface? = null

object SomeObject: by someObjectDelegate ?: SomeObjectImpl

object SomeObjectImpl : SomeInterface {

    fun someFun() {
        println("SomeObjectImpl someFun called")
    }
}

interface SomeInterface {
    fun someFun()
}

Dans vos tests, vous pouvez définir un objet délégué qui modifiera le comportement, sinon il utilisera sa mise en œuvre réelle. 

@Beofre
fun setUp() {
  someObjectDelegate = object : SomeInterface {
      fun someFun() {
          println("Mocked function")
      }
  }
  // Will call method from your delegate
  SomeObject.someFun()
}

Bien sûr, les noms ci-dessus sont mauvais, mais à titre d'exemple, cela montre le but.  

Après l’initialisation de SomeObject, le délégué gérera toutes les fonctions.
Vous trouverez plus d'informations dans les documents officiels documentation _

4
Ioane Sharvadze

En plus d’utiliser Mockk library, ce qui est très pratique, on peut se moquer d’une object simplement avec Mockito et la réflexion. Un objet Kotlin est juste une classe Java régulière avec un constructeur privé et un champ statique INSTANCE, avec une réflexion, on peut remplacer la valeur de INSTANCE par un objet simulé. Après le test, l'original doit être restauré pour que le changement n'affecte pas les autres tests.

Utiliser Mockito Kotlin (il faut ajouter une configuration d’extension comme décrit ici pour simuler les classes finales): 

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

Un premier amusement pourrait remplacer la valeur du champ statique INSTANCE dans la classe object et renvoyer la valeur précédente

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
}

Ensuite, vous pouvez vous amuser à créer une instance fictive de object et à remplacer la valeur d'origine par celle fausse, en renvoyant l'original pour qu'il puisse être réinitialisé ultérieurement.

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

Ajouter du sucre 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)

Et enfin, étant donné une object

object Foo {
    fun bar(arg: String) = 0
}

Vous pouvez le tester de cette façon

withMockObject(Foo.javaClass).test {
    doAnswer { 1 }.whenever(Foo).bar(any())

    Assert.assertEquals(1, Foo.bar(""))
}

Assert.assertEquals(0, Foo.bar(""))
3
lelloman

À moins de manipuler du code octet, la réponse est non, à moins que vous ne souhaitiez et puissiez modifier le code. Le moyen le plus direct (et que je recommanderais) de simuler l'appel de callerFun à SomeObject.someFun() est de fournir un moyen de lui faire glisser un objet fictif.

par exemple.

object SomeObject {
    fun someFun() {}
}

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

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

L'idée ici est de changer quelque chose que vous êtes prêt à changer. Si vous êtes certain de vouloir un singleton et une fonction de niveau supérieur qui agissent sur ce singleton, vous pouvez, comme indiqué ci-dessus, rendre testable la fonction de niveau supérieur sans changer sa signature publique: déplacer son implémentation vers une fonction internal. cela permet de glisser une maquette.

1
mfulton26