Тестирование функций расширения внутри классов

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

abstract class AbstractClass<T> {
    fun doStuff(): T = "Hello".foo()

    abstract fun String.foo(): T
}

class SubClass1: AbstractClass<Int>() {
    override fun String.foo(): Int = 1
}

class SubClass2: AbstractClass<Boolean>() {
    override fun String.foo(): Boolean = true
}

Как мы проверяем логику методов foo() в классах SubClass1 а также SubClass2? Это вообще возможно?

Я знаю, что могу изменить дизайн, чтобы проверить его. Две возможности произошли со мной:

  1. Не используйте функции расширения. ¯\_(ツ)_/¯

    abstract class AbstractClass<T> {
        fun doStuff(): T = foo("Hello")
    
        abstract fun foo(string: String): T
    }
    
    class SubClass1: AbstractClass<Int>() {
        override fun foo(string: String): Int = 1
    }
    

    Тогда мы можем создать объект SubClass1, вызов foo() и проверьте возвращаемое значение.

  2. Создать дополнительные функции расширения с internal видимость просто для проверки логики.

    class SubClass1: AbstractClass<Int>() {
        override fun String.foo(): Int = internalFoo()
    }
    
    internal fun String.internalFoo(): Int = 1
    

    Тогда мы можем создать объект String, вызов internalFoo() и проверьте возвращаемое значение. Тем не менее, мне не нравится это решение, потому что мы могли бы изменить тело override fun String.foo(): Int и наш тест пройдет.

Итак, возможно ли проверить функции расширения внутри классов? Если нет, как бы вы изменили свой дизайн, чтобы проверить их логику?

3 ответа

Решение

Поскольку тесты должны быть написаны с точки зрения клиента, я не уверен, что это будет правильный тест. Но я нашел один способ проверить это.

@Test
fun `test extension function`() {
    var int = 0

    SubClass1().apply {
        int = "blah".foo()
    }

    assertThat(int, `is`(1))
}

Методы расширения хороши, и они обеспечивают красивый свободный синтаксис, такой как в LINQ, Android KTX.

Однако методы расширения плохо работают с подклассами. Это потому, что расширение по существу не объектно-ориентировано. Они напоминают статические методы в Java, которые можно скрыть, но не переопределить в подклассах.

Если у вас есть какой-то код, который вы хотите изменить в подклассе, не пишите метод расширения, поскольку вы столкнетесь с препятствиями для тестирования, с которыми вы столкнулись в своем вопросе, и со всеми недостатками, которые статические методы приносят в тестируемость.

В вашем первом примере нет абсолютно ничего плохого и нет веской причины настаивать на методах расширения:

abstract class AbstractClass<T> {
    fun doStuff(): T = foo("Hello")

    abstract fun foo(string: String): T
}

class SubClass1: AbstractClass<Int>() {
    override fun foo(string: String): Int = 1
}

Решение @David, использующее функцию области видимости в экземпляре класса, который определяет функцию расширения, определенно работает для тестирования этого сценария. Однако возвращает объект контекста, а не результат лямбда-выражения, что требует объявленияvarво внешней области, которая затем назначается внутри лямбда-выражения. Если вы используете вместоapplyэто можно еще больше упростить, потому чтоrunвозвращает результат лямбда-выражения:

      val int = SubClass1().run {"blah".foo()}
assertThat(int, `is`(1))
Другие вопросы по тегам