Kotlin / Testability

Kotlin Testability – Part 2

The more observant regular readers of Styling Android will be aware that recently all of the sample code has switched from Java to Kotlin. There are many aspects of Kotlin which make our lives as developers much easier and recently I have been exploring how to make Kotlin classes easier to test. In this short series we’ll take a look in to some techniques which will can enormously assist in making our Kotlin classes testable.

Previously we looked at what I call the Default Factory Pattern and seen how it can benefit testability in pure Kotlin code, but it can also help us when it comes to testing Activities, Fragments, and custom Views.

The problem we have with these is that often the Android Framework is responsible for creating instances of them at runtime and so we cannot change the signature of the constructor to inject a Factory. With Kotlin we can quite easily use the Default Factory Pattern with, for example, a custom View (the same approach works for Activities & Fragments):

class CurrentDateTimeView internal constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = 0,
        private val factory: Factory,
        private val formatter: DateTimeFormatter = factory.getLocalisedDateTimeFormatter()
) : TextView(context, attrs, defStyleAttr) {

    @JvmOverloads constructor(
            context: Context,
            attrs: AttributeSet? = null,
            defStyleAttr: Int = 0) : this(context, attrs, defStyleAttr, DefaultFactory)

    fun update() {
        text = formatter.format(factory.getLocalDateTime())
    }

    interface Factory {
        fun getLocalDateTime(): LocalDateTime
        fun getLocalisedDateTimeFormatter(): DateTimeFormatter
    }

    private object DefaultFactory : Factory {
        override fun getLocalisedDateTimeFormatter(): DateTimeFormatter =
                DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL)

        override fun getLocalDateTime(): LocalDateTime = LocalDateTime.now()
    }
}

The principle is exactly the same as before – we use a primary constructor with limited access which can be used from our test cases, and then create a more openly accessible secondary constructor which is what should be used when we create this object in our production code. There is a nice feature of Kotlin that we use here: The @JvmOverloads annotation. This will generate the different permutations of constructor calls to match the different permutations of arguments. In this case having default values for attrs and defStyleAttr arguments will result in three separate Java constructors being generated:

  • CurrentDateTimeView(Context context)
  • CurrentDateTimeView(Context context, AttributeSet attrs)
  • CurrentDateTimeView(Context context, AttributeSet attrs, int defStyleAttr)

If you check the decompiled bytecode then there is actually more being generated than these because of the additional arguments on the primary constructor, but these are the ones needed by the Android Framework and so are the ones which will be called from our app code.

We can now write a unit test to specifically test the text of the TextView is set when we call the update() method:

class CurrentDateTimeViewTest {
    @Nested
    @DisplayName("Given a CurrentDateTimeView instance")
    inner class CurrentDateTimeViewInstance {
        private val context = MockContext()
        val currentDateTimeView = spy(CurrentDateTimeView(context, factory = factory))

        @Nested
        @DisplayName("When we call update()")
        inner class BuildDateString {

            @BeforeEach
            fun setup() {
                currentDateTimeView.update()
            }

            @Test
            @DisplayName("Then setText() should be called")
            fun notEmpty() {
                verify(currentDateTimeView, atLeastOnce()).text = any()
            }

            @Test
            @DisplayName("Then setText() should be called with \"$DATE_STRING\"")
            fun isEqual() {
                verify(currentDateTimeView, atLeastOnce()).text = stringCaptor.capture()

                assertThat(stringCaptor.value).isEqualTo(DATE_STRING)
            }
        }
    }

    object factory : CurrentDateTimeView.Factory {
        override fun getLocalDateTime(): LocalDateTime = mock {}

        override fun getLocalisedDateTimeFormatter(): DateTimeFormatter = mock {
            on {
                format(any())
            } doReturn DateStringProviderTest.DATE_STRING
        }
    }

    val stringCaptor: ArgumentCaptor<String> = ArgumentCaptor.forClass(String::class.java)

    companion object {
        const val DATE_STRING = "Date String"
    }
}

We can spy on the methods which are implemented on the super class to ensure that the interaction with the parent class is correct. However, this will not run by default as we’ll get a runtime error when we run the tests:

java.lang.RuntimeException: Method setText in android.widget.TextView not mocked. See http://g.co/androidstudio/not-mocked for details.

Even though we have set a spy on that particular method, by default there is not a mock implementation of it, so the tests do not run. However there is a really quick fix for this which requires a small addition to our build.gradle:

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'de.mannodermaus.android-junit5'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"


    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    testOptions {
        unitTests.returnDefaultValues = true
    }
}

junitPlatform {
    jupiterVersion '5.0.0-M6'
    platformVersion '1.0.0-M6'
}

dependencies {
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.jakewharton.threetenabp:threetenabp:1.0.5'
    testImplementation junitJupiter()
    testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
    testImplementation 'org.assertj:assertj-core:3.7.0'
    testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

repositories {
    mavenCentral()
}

Setting the returnDefaultValues flag to true means that a stubbed version of Android Framework classes is used at test-time so our tests now are able to run, and the spy works as expected. This is a useful little trick for testing classes which extend other classes which we don’t own. There is no point in writing test for third party code, and in this case we limit our testing to the scope of the functionality that we’ve added.

It is worth mentioning that the use of a Factory is only necessary in those instances where multiple instances of a specific class may be required at runtime, and the precise number is not known at compile-time. However there are also cases where we only have a single instance of a collaborator where we can still benefit from the practice of having a limited accessibility primary constructor with the collaborator instance supplied as an argument and this constructor is used for testing, and can be used to inject a mock of the collaborator. A secondary constructor with a production implementation and greater access is the public production API:

class MyClass internal constructor (collaborator: Collaborator) {
    constructor(): this(Collaborator())
}

Thanks to Sebastiano Poggi for an interesting discussion which lead to me adding this information about collaborator injection to the article.

The Default Factory Pattern is quite a useful mechanism for making our code testable – particularly when used with some other little tricks such as the Kotlin MockMaker and the returnDefaultValues flag.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.