JUnit 5 / Kotlin / Unit Testing

JUnit 5: Kotlin

JUnit 5 was formally released in July 2016 and is quite a major evolution to JUnit 4 which has been the standard unit testing framework since Android first appeared on the scene. There are some quite significant changes and getting things set up, and then getting the best out of JUnit 5 can require a little effort, but the results are well worth it. In this loosely-coupled series we’ll explore a few distinct aspects of JUnit 5 and look at how we can get the best out of it. In this article we’ll look at some of the new features in JUnit 5 and see how they can simplify our tests.

Previously we discussed the reasons behind choosing Kotlin to introduce lambdas into our tests, so let’s first look at how we configure our project to use Kotlin. One important thing here is while we want to use Kotlin for our tests, at this point we do not wish to use it in our main code base. So our configuration will go some way to enforcing that. Let’s start by adding Kotlin support to our top level build.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.1.1'
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'
        classpath 'de.mannodermaus.gradle.plugins:android-junit5:1.0.0-M3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

This makes the kotlin-gradle-plugin available to any child projects, and also defines a common kotlin_version variable so we use a consistent version of Kotlin throughout our child projects. Next let’s apply this to our app module:

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

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.stylingandroid.junit5"
        minSdkVersion 23
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        test.java.srcDirs += 'src/test/kotlin'
    }
}

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

dependencies {
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile junitJupiter()
    testProvided "org.junit.vintage:junit-vintage-engine:4.12.0-M3"
    testCompile 'org.assertj:assertj-core:3.6.2'
    testCompile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

apply from: '../config/static_analysis.gradle'

First we apply the Kotlin plugin so that it will be run as part of the module build, then we define an additional kotlin directory within our test code which will contain any Kotlin tests, and finally we include the Kotlin stdlib within test compile.

It is these final two things which go some way to enforcing how Kotlin can only be used on test code.

Firstly we only add a kotlin source set. This is more a best practice / guidance issue than hard enforcement. It makes sense to separate our Java sources from our Kotlin sources, and this does precisely that. However, it is worth bearing in mind that the Kotlin compiler will run on Kotlin source that is placed in the existing Java source set, so this will not prevent any Kotlin source being added in to the production source. However, having no Kotlin source set should make it easier to spot during code review, which is why I have chosen to do this.

The actual enforcement is done more by how the kotlin-stdlib dependency is added to only testCompile. Although the Kotlin compiler will actually run on our main code source, the stdlib contains many of the standard features that add to the usefulness and fluency of Kotlin, such as annotations, and collection classes. Kotlin is rather restricted without the stdlib. So by ensuring that it is unavailable to our main code, the Kotlin compiler will throw errors if Kotlin code which has dependencies on stdlib exists in the main code source set.

So let’s now convert our test to Kotlin:

package com.stylingandroid.junit5

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.TestFactory
import java.util.Locale
import kotlin.collections.LinkedHashSet

@SuppressWarnings("unused")
class CalculatorJUnit5KotlinTest {
    private var calculator = Calculator()
    private val input1 = 1f

    @TestFactory
    @DisplayName("Given inputs of One and Two")
    internal fun oneAndTwo(): Collection<DynamicTest> {
        val input2 = 2f
        val tests = LinkedHashSet<DynamicTest>(6)
        tests.add(createTest(input1, input2, Operator.PLUS, 3f))
        tests.add(createTest(input1, input2, Operator.MINUS, -1f))
        tests.add(createTest(input1, input2, Operator.MULTIPLY, 2f))
        tests.add(createTest(input1, input2, Operator.DIVIDE, 0.5f))
        tests.add(createTest(input1, input2, Operator.MODULO, 1f))
        tests.add(createTest(input1, input2, Operator.UNKNOWN, 0f))
        return tests
    }

    @TestFactory
    @DisplayName("Given inputs of One and Zero")
    internal fun oneAndZero(): Collection<DynamicTest> {
        val input2 = 0f
        val tests = LinkedHashSet<DynamicTest>(6)
        tests.add(createTest(input1, input2, Operator.PLUS, 1f))
        tests.add(createTest(input1, input2, Operator.MINUS, 1f))
        tests.add(createTest(input1, input2, Operator.MULTIPLY, 0f))
        tests.add(createTest(input1, input2, Operator.DIVIDE, Float.POSITIVE_INFINITY))
        tests.add(createTest(input1, input2, Operator.MODULO, Float.NaN))
        tests.add(createTest(input1, input2, Operator.UNKNOWN, 0f))
        return tests
    }

    private fun createTest(value1: Float, value2: Float, operator: Operator, expected: Float): DynamicTest {
        val displayName = String.format(Locale.getDefault(), "When we %s them Then the result is %.2f", operator.name, expected)
        return dynamicTest(displayName) {
            val result = calculator.calculate(value1, value2, operator)

            assertThat(result).isEqualTo(expected)
        }
    }
}

I’ll be completely open and honest here: I simply ran the existing JUnit5 Java test through the convert Java to Kotlin tool which ships with the Kotlin IDEA / Android Studio plugin. The first thing to note is that we import LinkedHashSet which is in stdlib. So even this relatively simple code would start giving errors if we moved it in to our main source set! The other main this is that we have replaced the creating of an Executable instance with a much more compact lambda.

The net result is that it has removed a further 15 lines of code – the Java test suite is 68 lines, the Kotlin one is 53 lines.

At this point it would be tempting to explore the features of Kotlin (such as the functional operators on Kotlin collections) to see how we could further reduce our test code down. However, this series is more about harnessing the power of JUnit5, so we’ll stop here.

Notwithstanding any further Kotlin tricks, we have still gone from an initial JUnit 4 test suite which clocked in at 137 lines of code, and reduced it down to 53 lines. The tests are also far more readable and understandable as a result, and our test suite really does feel much more like documentation of how our Calculator class is expected to work – which was one of the aims. Moreover, I have a suspicion that attempting to reduce this further may actually begin to reduce the readability, which was another factor in my decision to stop here.

Overall JUnit 5 provides really improves on the tools that we have available in JUnit 4, and even without direct support within the Android build toolchain, Marcel Schnelle’s plugin is an excellent enabler which allows us to use to in Android projects.

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.

5 Comments

  1. Thank you for this writeup. I’d like to know: what is the biggest advantage of using Kotlin here (instead of Java)? Besides it making the code succinct, is there something Kotlin did here that Java couldn’t do?

      1. So it’s really essentially for more concise code and readability (and I guess less dependency on things like Retrolambda just to use lambdas)?

        Is it possible to write tests for existing Java code with Kotlin?

        1. Yes it is. Kotlin will compile down to standard Java bytecode, so the object under test has no dependency on Kotlin.

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.