Если мы хотим протестировать функцию расширения для типа, мы можем создать экземпляр этого типа, вызвать функцию и проверить возвращаемое значение. Но как насчет тестирования функций расширения, определенных внутри классов?
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
? Это вообще возможно?
Я знаю, что могу изменить дизайн, чтобы проверить это. Мне пришли в голову две возможности:
Не используйте функции расширения. ¯ \ _ (ツ) _ / ¯
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()
и проверить возвращаемое значение.
Создайте дополнительные функции расширения с видимостью internal
просто для проверки логики.
class SubClass1: AbstractClass<Int>() {
override fun String.foo(): Int = internalFoo()
}
internal fun String.internalFoo(): Int = 1
Затем мы можем создать объект String
, вызвать internalFoo()
и проверить возвращаемое значение. Однако мне не нравится это решение, потому что мы могли бы изменить корпус override fun String.foo(): Int
, и наш тест прошел бы.
Итак, можно ли тестировать функции расширения внутри классов? Если нет, как бы вы изменили свой дизайн, чтобы проверить их логику?
Поскольку тесты должны быть написаны с точки зрения клиента, я не уверен, что это будет действительный тест. Но я придумал один способ проверить это.
@Test
fun `test extension function`() {
var int = 0
SubClass1().apply {
int = "blah".foo()
}
assertThat(int, `is`(1))
}
О, мы действительно можем их протестировать! Этот код компилируется, потому что тип функции { int = "blah".foo() }
- SubClass1.() -> Unit
, и он работает так, как если бы код блока был вычислен внутри SubClass1
, который является приемником функции. Подробнее о ресиверах в Котлине в stackoverflow.com/a/45875492/1861769.
Что, если у функции переопределения есть параметры? Как бы вы включили параметры?
Методы расширения великолепны, и они обеспечивают красивый плавный синтаксис, как в 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
}
Спасибо за ваш ответ и ссылки. Подводя итог, не стоит злоупотреблять функциями расширения.
Можете ли вы подробнее рассказать о своем варианте использования? Тогда намного проще давать какие-либо рекомендации.