Как издеваться над одиночным объектом Kotlin?

Учитывая одноэлементный объект Kotlin и удовольствие, которое вызывает его метод

object SomeObject {
   fun someFun() {}
}

fun callerFun() {
   SomeObject.someFun()
}

Есть ли способ издеваться надSomeObject.someFun()?

Ответы на вопрос(5)

Решение Вопроса

тогда вы можете смоделировать ваш объект с помощью любой библиотеки. Вот пример Junit + Mockito +Mockito-Котлин:

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)
    }
}
 Ruslan20 янв. 2018 г., 19:23
Не дополнительные классы, просто интерфейсы. И вы должны сделать это, потому что это лучшая практика для тестирования на JVM.
 Kerooker20 янв. 2018 г., 17:19
Это похоже на анти-шаблон ... Не следует создавать дополнительные классы / интерфейсы в основном коде только для тестирования!
 Bastian Voigt27 февр. 2019 г., 21:21
С mockK вы можете сделать это намного чище ...

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

например

object SomeObject {
    fun someFun() {}
}

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

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

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

Mockk, который позволяет вам высмеивать объекты, так же, как вы хотите.

По состоянию на документацию:

Объекты могут быть преобразованы в макеты следующим образом:

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

Несмотря на ограничения языка Kotlin, вы можете создавать новые экземпляры объектов, если для проверки логики требуется следующее:

val newObjectMock = mockk<MockObj>()

Mockk библиотека, что довольно удобно, можно издеваться надobject просто с мокито и отражением. Объект 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 и замените исходное значение на поддельное, возвращая исходное значение, чтобы его можно было позже сбросить

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(""))
 lelloman19 мар. 2018 г., 08:10
Ну, вам не нужно менятьвсе ваши тестыТолько насмешливая функция. Но самое главное, если реализацияobject изменилось, вам нужно будет перекомпилировать старый код. Если вы компилируетеFoo.bar() сегодня скомпилированный результат будет использоватьINSTANCE поле также.
 azizbekian19 мар. 2018 г., 07:46
По сути, вы полагаетесь на детали реализации Kotlin: каждый раз, когда дизайнеры Kotile решают изменить реализациюobject классы, то вы должны обновить все свои тесты.
 Ch Vas17 сент. 2018 г., 16:00
Это также работает для меня! Использование mockito-kotlin2.0.0-RC2, не проверил, если версия 2 предоставляет что-то более прямолинейное, хотя.

и, используя классделегаты.

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

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

 Kerooker29 авг. 2019 г., 05:59
Теоретически, когда вы делаете это, вы также должны проверять, что ваши делегаты работают правильно, и для написания теста для этого также потребуется делегат и так далее. Используя ложную альтернативу, мы верим, что тестовая библиотека сама тестируется, и нам не нужно добавлять дополнительные шаблоны и дополнительные линии для этой концепции

Ваш ответ на вопрос