Как насмехаться над объектом Singleton Kotlin?

Учитывая объект Singleton Kotlin и удовольствие, вызывающее его метод

object SomeObject {
   fun someFun() {}
}

fun callerFun() {
   SomeObject.someFun()
}

Есть ли способ высмеять вызов SomeObject.someFun()?

Ответ 1

Просто создайте объект, реализующий интерфейс, чем вы можете издеваться над объектом с любой насмешливой библиотекой. Вот пример 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)
    }
}

Или в случае, если вы хотите mock 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)
    }
}

Ответ 2

Есть очень хорошая библиотека для издевательств над 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>()

Ответ 3

Вы можете макетировать объект без какой-либо дополнительной библиотеки, используя делегаты класса.

Вот мое предложение

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 делегат будет обрабатывать все функции.
Больше вы можете найти в официальной документации

Ответ 4

Кроме использования Mockk библиотека, что довольно удобно, можно было бы издеваться над object просто с Mockito и отражением. Объект Kotlin - это просто обычный Java-класс с частным конструктором и статическим полем INSTANCE, с отражением можно заменить значение INSTANCE на насмеханный объект. После теста оригинал должен быть восстановлен, чтобы изменение не повлияло на другие тесты.

Использование Mockito Kotlin (нужно добавить конфигурацию расширения, как описано здесь, чтобы высмеять финальные классы):

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

Первое удовольствие может заменить значение статического поля INSTANCE в классе 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 и замените исходное значение на посмеянный, вернув оригинал, чтобы он мог быть reset later

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

Ответ 5

Недостаточно манипулировать байтовым кодом, нет ответа, если вы не хотите и не можете изменить код. Самый простой способ (и способ, который я рекомендовал бы), чтобы высмеивать вызов callerFun на SomeObject.someFun(), - это предоставить некоторый способ пропустить его макет объекта.

например.

object SomeObject {
    fun someFun() {}
}

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

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

Идея здесь состоит в том, чтобы изменить то, что вы готовы изменить. Если вы уверены, что хотите один синглтон и функцию верхнего уровня, действующие на этом синглетоне, то один из способов, как было продемонстрировано выше, сделать функцию верхнего уровня проверкой без изменения ее публичной подписи - переместить ее реализацию в internal, которая позволяет скользить макет.