Home > front end >  How to unit test a callback in parameter - Kotlin
How to unit test a callback in parameter - Kotlin

Time:09-17

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?

CodePudding user response:

The only way I can think of is to add a boolean flag in callback's onReady() method. So it will become:

In library:

interface ReadyCallback {
    void onReady(final boolean isReady)
}

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

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 { isReady -> onReady(isReady) }
    }

    override fun onReady(isReady: Boolean) {
        if (isReady) {
            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(true)
        unitUnderTest.createClient()
        verify(classCMock).otherMethod()
    }

    @Test
    fun `when create client and not ready then do not call otherMethod`() {
        unitUnderTest.onReady(false)
        unitUnderTest.createClient()
        verifyZeroInteractions(classCMock)
    }
}

But I still don't know how to test without the boolean parameter in the callback's method. Does anyone know how to do that?

CodePudding user response:

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)
    }
}
  • Related