Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test lambda functions in Android with Kotlin

I need to Unit-Test a function doClassAction and inside it calls the function doStoreAction on the object store. This doStoreAction accepts two parameters, one integer and one function, like in the example below

class MyStore() {

    fun doStoreAction(num: Int, callback: (text: String) -> Unit) {
        // It does something 
        // ...
        // and when it's done it calls the callback

        return callback("some text")
    }
}

class MyClass(private val store: MyStore,
              private val objectX: MyObject) {

    fun doClassAction(num: Int) {
        store.doStoreAction(num) { callbackResult ->
            // Action (potentially asynchronous)
            // that will happen after obtaining the value via the callback

            objectX.doObjectAction(callbackResult)
        }
    }
}

I am using Mockito and so far my test has the following lines

@Test
fun doClassActionTest() {
    val mockStore = mock(MyStore::class.java)
    val mockObject = mock(MyObject::class.java)
    val class = MyClass(mockStore, mockObject)
    class.doClassAction(42)
}

But it doesn't compile because the call to the method does not specify the callback... I don't want to overwrite it since I want the test to use the original one and verify that the things inside are called properly, like:

when(objectX.doObjectAction(anyString())).doNothing()
verify(objectX, times(1)).doObjectAction(callbackResult)

Is there a way to call the method and also mock the callback response to a value I set in the tests but still have it go through the original code flow?

like image 719
kioli Avatar asked May 09 '18 15:05

kioli


1 Answers

After days of trying out things and reading about it, I finally found a solution that works and I'll share it here for whomever find themselves in the same pickle

@Test
fun doClassActionTest() {
    val mockStore = mock(MyStore::class.java)
    val mockObject = mock(MyObject::class.java)
    val myClass = MyClass(mockStore, mockObject)
    val input = 42
    val resultCallback = "test result"

    `when`(mockStore.doStoreAction(any(), any())).then { invocation ->
        (invocation.arguments[1] as (String) -> Unit).invoke(resultCallback)
    }

    myClass.doClassAction(input)

    verify(mockObject, only()).doObjectAction(resultCallback)
}

The caveat here is to remember to put the classes that you are mocking AND the methods called by those classes as open in Kotlin otherwise there could be false positives

P.s. I also used few inline functions extracted from the mockito-kotlin library such as:

inline fun <reified T : Any> any() = Mockito.any(T::class.java) ?: createInstance<T>()

inline fun <reified T : Any> createInstance(): T = createInstance(T::class)

fun <T : Any> createInstance(kClass: KClass<T>): T = castNull()

@Suppress("UNCHECKED_CAST")
private fun <T> castNull(): T = null as T
like image 137
kioli Avatar answered Oct 23 '22 14:10

kioli