Tricky parts when implementing unit tests for Android in Kotlin

Testing data classes in Kotlin

As we all know, data classes are final by default on Kotlin and if you try to use Mockito and mock such a class, you will get an exception. What you need to do is add this file:

And inside this file you need the following content

mock-maker-inline

Only that row above. This will allow mocking final classes.

Testing RxJava Completable

Sometimes using the verify() method for a Completable in mockito doesn’t work as you would expect it to. You need to assert that the completable was subscribed to and executed. To do that you can use this class:

import io.reactivex.Completable
import io.reactivex.CompletableObserver
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue

class TestCompletable(private val autocomplete: Boolean = true) : Completable() {

    private var subscribed = false

    private val wrapped = create {
        subscribed = true
        if (autocomplete) {
            it.onComplete()
        }
    }

    override fun subscribeActual(s: CompletableObserver) {
        wrapped.subscribe(s)
    }

    fun assertSubscribed() {
        assertTrue(subscribed)
    }

    fun assertNotSubscribed() {
        assertFalse(subscribed)
    }
}

So now, basically whenever you have a method that returns this TestCompletable, you can verify it was subscribed to like this:

private val revokeCompletable = TestCompletable()
private val myApi: MyApi = mock {
    on(it.revokeToken(any(), any(), any())).thenReturn(revokeCompletable)
}

@Test
fun logOutUser_revokesTokens() {
    ***
    val testObserver = underTest.logOutUser().test()
    ***
    revokeCompletable.assertSubscribed()
}

Tests for LiveData and final classes

More information can be found HERE.

Timezone Tests with ThreeTen

org.threeten.bp.zone.ZoneRulesException: No time-zone data files registered

To fix this exception you need to have the TZDB.dat file inside your resources folder for the unit tests. Then you need to call the initThreeTen method which does this:

import org.threeten.bp.zone.TzdbZoneRulesProvider
import org.threeten.bp.zone.ZoneRulesProvider

fun Any.initThreeTen() {
    if (ZoneRulesProvider.getAvailableZoneIds().isEmpty()) {
        val stream = this.javaClass.classLoader!!.getResourceAsStream("TZDB.dat")
        stream.use(::TzdbZoneRulesProvider).apply {
            ZoneRulesProvider.registerProvider(this)
        }
    }
}
  1. To get the TZDB.dat file you can use this utility to convert the timezone files into a single binary file that can be used by the Open JDK.
  2. The TZDB.dat file needs to be added here:

What you need to do before each test is init the three ten library

@BeforeEach
fun initThreeTenLib() {
    initThreeTen()
}

Mocking resources

Mocking resources every time can become nasty and annoying. So here are some helper functions that you can use when you want to mock the Resources class

/**
 * When a string without parameters is requested, return it in this form "{stringId}"
 *
 * When a string with one param is requested, return it in this form "{stringId}:{parameter}"
 *
 * When a plural with one param is requested, return it in this form "{stringId}:{quantity}:{parameter}"
 */
fun simulateStringFetching(resources: Resources) {
    fun join(invocation: InvocationOnMock) = invocation.arguments.joinToString(":")
    whenever(resources.getText(anyInt()))
        .then { join(it) }
    whenever(resources.getString(anyInt()))
        .then { join(it) }
    whenever(resources.getString(anyInt(), any()))
        .then { join(it) }
    whenever(resources.getString(anyInt(), any(), any()))
        .then { join(it) }
    whenever(resources.getString(anyInt(), any(), any(), any()))
        .then { join(it) }
    whenever(resources.getString(anyInt(), any(), any(), any(), any()))
        .then { join(it) }
    whenever(resources.getQuantityString(anyInt(), anyInt()))
        .then { it.arguments.joinToString(":") }
    whenever(resources.getQuantityString(anyInt(), anyInt(), anyVararg()))
        .then { it.arguments.joinToString(":") }
}

/**
 * When a string without parameters is requested, return it in this form "{stringId}"
 *
 * When a string with one param is requested, return it in this form "{stringId}:{parameter}"
 */
fun simulateStringFetching(context: Context) {
    fun join(invocation: InvocationOnMock) = invocation.arguments.joinToString(":")
    whenever(context.getText(anyInt()))
        .then { join(it) }
    whenever(context.getString(anyInt()))
        .then { join(it) }
    whenever(context.getString(anyInt(), any()))
        .then { join(it) }
    whenever(context.getString(anyInt(), any(), any()))
        .then { join(it) }
    whenever(context.getString(anyInt(), any(), any(), any()))
        .then { join(it) }
    whenever(context.getString(anyInt(), any(), any(), any(), any()))
        .then { join(it) }
}

fun mockContextWithStringsAndIntegersFetching(): Context {
    val context = mock(Context::class.java)
    simulateStringFetching(context)
    whenever(context.getColor(anyInt())).thenAnswer { it.arguments[0] }

    val resources = mockResourcesWithStringsAndIntegersFetching()
    whenever(context.resources).thenReturn(resources)
    return context
}

fun mockResourcesWithStringsAndIntegersFetching(): Resources {
    val resources = mock(Resources::class.java)
    simulateStringFetching(resources)

    whenever(resources.getColor(anyInt(), anyOrNull()))
        .then { it.arguments[0] }

    whenever(resources.getInteger(anyInt()))
        .then { it.arguments[0] }

    return resources
}

and then you can use them like this:

assertThat(underTest.moneyStringObservable.get())
  .isEqualTo("${R.string.money_fixed_value}:$ 100")

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s