É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()
?
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)
}
}
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>()
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 _
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(""))
À 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.