Search code examples
javaunit-testingkotlincallbackmockito-kotlin

How to unit test a callback in parameter - Kotlin


I have a Client class (written in Kotlin in an Android app) that implements an interface ReadyCallback (written in Java in a library of the app, the app is dependent on this library). In Client I have a createClient() method which will create a client with the parameter of ReadyCallback. If it's ready, I will perform other tasks by calling classC.otherMethod(), if not ready, I just create the client without doing other stuff:

In the library:

// Somewhere in this library, I have logic to call `readyCallback.onReady()` when I consider it's "ready"
interface ReadyCallback {
    void onReady()
}

class Manager {
    private final ReadyCallback readyCallback;
    public void onConnected(final boolean isConnected) {
        if (isConnected) {
            readyCallback.onReady();
        }
    }
}

In the app:

class ClassA internal constructor(private val clientProvider: ClassB, private val classC: ClassC, private val classD: ClassD) : ReadyCallback {
    fun createClient() {
        val client = clientProvider.create(getReadyCallback())
    }

    private fun getReadyCallback() {
        return ReadyCallback { onReady() }
    }

    override fun onReady() {
        logInfo { "It's ready! Now do some stuff by calling classC.otherMethod()" }
        classC.otherMethod()
    }
}

In unit test, I want to verify that when I create the client and it's ready, classC's otherMethod() will be invoked. I tried to do the following but it's not correct:

import com.nhaarman.mockitokotlin2.*
import org.junit.*

class ClassATest {
    lateinit var unitUnderTest: ClassA
    lateinit var clientProviderMock: ClassB
    lateinit var classCMock: ClassC
    lateinit var clientMock: ClassD

    @Before
    override fun setup() {
        super.setup()
        clientProviderMock = mock()
        classCMock = mock()
        clientMock = mock()
        unitUnderTest = ClassA(clientProvider = clientProviderMock, classC = classCMock, classD = classDMock)

        whenever(clientProviderMock.create(any()).thenReturn(client)
    }

    @Test
    fun `when create client then call otherMethod`() {
        unitUnderTest.createClient()
        verify(classCMock).otherMethod()
    }
}

The error message shows:

Wanted but not invoked:
classC.otherMethod();
Actually, there were zero interactions with this mock.

I think the reason I got this error is because, if I don't call getReadyCallback(), it means I am not invoking the callback, so there's no call to classC.otherMethod(). But other than that I am really stuck on this, I don't know how to unit test my desire behavior (If it's ready, classC.otherMethod() will be called, if not ready, this method won't be called).

I know I can't do things like below because unitUnderTest is not a mock object:

callbackMock = mock()
whenever(unitUnderTest.getReadyCallback()).thenReturn(callbackMock)
whenever(clientProviderMock.create(callbackMock).thenReturn(client)

Can anyone help me out please?


Solution

  • I think I figured it out. I don't need a parameter in onReady(). In library:

    interface ReadyCallback {
        void onReady()
    }
    
    // place to determine when is "ready"
    class Manager {
        private final ReadyCallback readyCallback;
        public void onConnected(final boolean isConnected) {
            if (isConnected) {
                readyCallback.onReady();
            }
        }
    }
    

    In app:

    class ClassA internal constructor(private val clientProvider: ClassB, private val classC: ClassC, private val classD: ClassD) : ReadyCallback {
        fun createClient() {
            val client = clientProvider.create(getReadyCallback())
        }
    
        private fun getReadyCallback() {
            return ReadyCallback { onReady() }
        }
    
        override fun onReady() {
            logInfo { "It's ready! Now do some stuff by calling classC.otherMethod()" }
            classC.otherMethod()
        }
    }
    

    In unit test:

    import com.nhaarman.mockitokotlin2.*
    import org.junit.*
    
    class ClassATest {
        lateinit var unitUnderTest: ClassA
        lateinit var clientProviderMock: ClassB
        lateinit var classCMock: ClassC
        lateinit var clientMock: ClassD
    
        @Before
        override fun setup() {
            super.setup()
            clientProviderMock = mock()
            classCMock = mock()
            clientMock = mock()
            unitUnderTest = ClassA(clientProvider = clientProviderMock, classC = classCMock, classD = classDMock)
    
            whenever(clientProviderMock.create(any()).thenReturn(client)
        }
    
        @Test
        fun `when create client and ready then call otherMethod`() {
            unitUnderTest.onReady()
            unitUnderTest.createClient()
            verify(classCMock).otherMethod()
        }
    
        @Test
        fun `when create client and not ready then do not call otherMethod`() {
            unitUnderTest.createClient()
            verifyZeroInteractions(classCMock)
        }
    }