Testing extension functions inside classes
Asked Answered
C

3

12

If we want to test an extension function on a type, we can create an instance of this type, call the function and check the returned value. But what about testing extension functions defined inside classes?

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
}

How do we test the logic of the methods foo() in classes SubClass1 and SubClass2? Is it even possible?

I know I can change the design to test it. Two possibilities have occurred to me:

  1. Don't use extension functions. ¯\_(ツ)_/¯

    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
    }
    

    Then we can create an object SubClass1, call foo() and check the returned value.

  2. Create additional extension functions with internal visibility just to test the logic.

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

    Then we can create an object String, call internalFoo() and check the returned value. However, I don't like this solution because we could change the body of override fun String.foo(): Int and our test would pass.

So, is it possible to test extension functions inside classes? If not, how would you change your design in order to test their logic?

Congreve answered 5/6, 2018 at 14:21 Comment(1)
Can you write more about your use case? Then it's much easier to give any recommendations.Preparedness
G
18

Since tests should be written from the client's perspective, I'm not sure it would be a valid test. But I did come up with one way to test it.

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

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

    assertThat(int, `is`(1))
}
Gabar answered 5/6, 2018 at 16:28 Comment(2)
Oh, we can actually test them! This code compiles because the type of the function { int = "blah".foo() } is SubClass1.() -> Unit and it works as if the block code was evaluated inside SubClass1, which is the receiver of the function. More info about receivers in Kotlin in https://mcmap.net/q/181081/-what-is-a-quot-receiver-quot-in-kotlin.Congreve
What if the override function has parameters? How would you include the parameters?Greiner
B
3

Extension methods are great and they enable beautiful fluent syntax like that in LINQ, Android KTX.

However, extension methods do not work well with subclassing. This is because extension are essentially non-object-oriented. They resemble the static methods in Java that can be hidden but not overridden in subclasses.

If you have some code you expect to change in a subclass, don't write an extension method as you will face the barriers to testing you have experienced yourself in your question and all of the disadvantages that static methods brings to testability.

There is absolutely nothing wrong with your first example and no good reason to insist on extension methods there:

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
}
Beanpole answered 6/6, 2018 at 7:53 Comment(1)
Thanks for your answer and the references. To sum up, we shouldn't abuse extension functions.Congreve
T
2

@David's solution using the apply scope function on an instance of the class that defines the extension function definitely works for testing this scenario. However, apply returns the context object, not the result of the lambda expression, which necessitates declaring a var in the enclosing scope which is then assigned inside the lambda expression. If you use run instead of apply this can be further simplified, because run returns the result of the lambda expression:

val int = SubClass1().run {"blah".foo()}
assertThat(int, `is`(1))
Ticon answered 13/5, 2023 at 23:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.