Android Testing with Kotlin

03 Feb 2017

This is not a new topic actually, especially since Kotlin is gaining terrain in the world of programming languages in general, and especially on Android. Also, I'm not going to get into details on what Kotlin offers since there is people out there doing a great job (especial mention to my friend Antonio Leiva).

Kotlin Programming Language

Before starting I will just mention and give a quick summary of the main benefits of this "young?" and modern programming language:

Why Kotlin in Tests?

We have an android codebase in our old and lovely? Java and we would love to introduce this awesome language gradually, so why not starting with tests? This way we can give it a try without affecting our main application under any circumstances, and at the same time we get the excitement of a modern and already mature language, plus some training for us and our team in preparation for the big change. Sounds good right? Let's write some code then...

Getting our hands dirty

Basically, the idea is to showcase how we can test our android applications using Kotlin, so as a first step we need to setup and prepare our environment by adding Kotlin dependencies in our build.gradle file:

buildscript {
  repositories {
    mavenCentral()
    jcenter()
  }
  dependencies {
    classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.5-2'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'

...

dependencies {
  ...
  compile "org.jetbrains.kotlin:kotlin-stdlib:1.0.6"

  ...
  testCompile 'org.jetbrains.kotlin:kotlin-stdlib:1.0.6'
  testCompile 'org.jetbrains.kotlin:kotlin-test-junit:1.0.6'
  testCompile "com.nhaarman:mockito-kotlin:1.1.0"
  testCompile 'org.amshove.kluent:kluent:1.14'
}

Now we need to set the dedicated directories for tests written in Kotlin, this is done in our sourceSets section:

android {
  ...
  sourceSets {
    test.java.srcDirs += 'src/test/kotlin'
    androidTest.java.srcDirs += 'src/androidTest/kotlin'
  }
  ...
}

And as a third step we want to make sure, we do not allow accidentally (for now ;)) any Kotlin code in production by also adding this defensive lines:

afterEvaluate {
  android.sourceSets.all { sourceSet ->
    if (!sourceSet.name.startsWith('test') || !sourceSet.name.startsWith('androidTest')) {
      sourceSet.kotlin.setSrcDirs([])
    }
  }
}

The entire file can be seen in the sample project on Github. And now we are able to write tests in the same way as we would do in Java.

JUnit Tests

What I only need here is JUnit, Mockito-kotlin and Kluent (a library with really cool assertion semantics).

Let's see a simple test case for a class called GetUserDetails.java which is a UseCase (from a Clean Architecture approach) in my application:

class GetUserDetailsTest {

  private val USER_ID = 123

  private lateinit var getUserDetails: GetUserDetails

  private val userRepository: UserRepository = mock()
  private val threadExecutor: ThreadExecutor = mock()
  private val postExecutionThread: PostExecutionThread = mock()

  @Before
  fun setUp() {
    getUserDetails = GetUserDetails(userRepository, threadExecutor, postExecutionThread)
  }

  @Test
  fun shouldGetUserDetails() {
    getUserDetails.buildUseCaseObservable(GetUserDetails.Params.forUser(USER_ID));

    verify(userRepository).user(USER_ID)
    verifyNoMoreInteractions(userRepository)
    verifyZeroInteractions(postExecutionThread)
    verifyZeroInteractions(threadExecutor)
  }
}

Something to pay attention is that when we need to construct our subject under test (in the setup method), we must declare it lateinit“, otherwise the compiler will complain, since properties must be initialized or be abstract. Here is another test example for a Serializer.java class in the project, where you can see the assertions mentioned above:

class SerializerTest {

  private val JSON_RESPONSE = "{\n \"id\": 1,\n " +
                              "\"cover_url\": \"http://www.android10.org/myapi/cover_1.jpg\",\n " +
                              "\"full_name\": \"Simon Hill\",\n " +
                              "\"description\": \"Curabitur gravida nisi at nibh. In hac habitasse " +
                              "platea dictumst. Aliquam augue quam, sollicitudin vitae, consectetuer " +
                              "eget, rutrum at, lorem.\\n\\nInteger tincidunt ante vel ipsum. " +
                              "Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo " +
                              "placerat.\\n\\nPraesent blandit. Nam nulla. Integer pede justo, " +
                              "lacinia eget, tincidunt eget, tempus vel, pede.\",\n " +
                              "\"followers\": 7484,\n " +
                              "\"email\": \"[email protected]\"\n}"

  private var serializer = Serializer()

  @Test
  fun shouldSerialize() {
    val userEntityOne = serializer.deserialize(JSON_RESPONSE, UserEntity::class.java)
    val jsonString = serializer.serialize(userEntityOne, UserEntity::class.java)
    val userEntityTwo = serializer.deserialize(jsonString, UserEntity::class.java)

    userEntityOne.userId shouldEqual userEntityTwo.userId
    userEntityOne.fullname shouldEqual userEntityTwo.fullname
    userEntityOne.followers shouldEqual userEntityTwo.followers
  }

  @Test
  fun shouldDesearialize() {
    val userEntity = serializer.deserialize(JSON_RESPONSE, UserEntity::class.java)

    userEntity.userId shouldEqual 1
    userEntity.fullname shouldEqual "Simon Hill"
    userEntity.followers shouldEqual 7484
  }
}

Robolectric (Integration?) Tests

I created a test parent class (used for each test case) in order to encapsulate everything Robolectic related, thus, my tests do not depend directly on this framework. The idea is that any functionality or helper method is wrapped here (this is a lesson learned from the past where I polluted all my code with Robolectric classes, so the process of migrating to a non-backward compatible newer version was very painful).

/**
 * Base class for Robolectric data layer tests.
 * Inherit from this class to create a test.
 */
@RunWith(RobolectricTestRunner::class)
@Config(constants = BuildConfig::class,
        application = AndroidTest.ApplicationStub::class,
        sdk = intArrayOf(21))
abstract class AndroidTest {

  fun context(): Context {
    return RuntimeEnvironment.application
  }

  fun cacheDir(): File {
    return context().cacheDir
  }

  internal class ApplicationStub : Application()
}

And here is an example of an integration test that interacts with the android framework through our AndroidTest.kt class:

class FileManagerTest : AndroidTest() {

  private var fileManager = FileManager()

  @After
  fun tearDown() {
    fileManager.clearDirectory(cacheDir())
  }

  @Test
  fun shouldWriteToFile() {
    val fileToWrite = createDummyFile()
    val fileContent = "content"

    fileManager.writeToFile(fileToWrite, fileContent)

    fileToWrite.exists() shouldEqualTo true
  }

  @Test
  fun shouldHaveCorrectFileContent() {
    val fileToWrite = createDummyFile()
    val fileContent = "content\n"

    fileManager.writeToFile(fileToWrite, fileContent)
    val expectedContent = fileManager.readFileContent(fileToWrite)

    expectedContent shouldEqualTo fileContent
  }

  private fun createDummyFile(): File {
    val dummyFilePath = cacheDir().path + File.separator + "dummyFile"
    return File(dummyFilePath)
  }
}

Espresso Acceptance (UI?) Tests

My choice here is Espresso since it is backed by Google and from my perspective the most stable integration test framework nowadays. The same as with Robolectric, I decided to create a little framework on top of it, let's see how it works. All my tests depend on an AcceptanceTest.kt class:

@LargeTest
@RunWith(AndroidJUnit4::class)
abstract class AcceptanceTest<T : Activity>(clazz: Class<T>) {

  @Rule @JvmField
  val testRule: ActivityTestRule<T> = IntentsTestRule(clazz)

  val checkThat: Matchers = Matchers()
  val events: Events = Events()
}

Things to keep an eye on here:

class Matchers {
  fun <T : Activity> nextOpenActivityIs(clazz: Class<T>) {
    intended(IntentMatchers.hasComponent(clazz.name))
  }

  fun viewIsVisibleAndContainsText(@StringRes stringResource: Int) {
    onView(withText(stringResource)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
  }

  fun viewContainsText(@IdRes viewId: Int, @StringRes stringResource: Int) {
    onView(withId(viewId)).check(matches(withText(stringResource)))
  }
}
class Events {
  fun clickOnView(@IdRes viewId: Int) {
    onView(withId(viewId)).perform(click())
  }
}

Last but not least, an example of the main activity of the project, which displays a view and also opens another activity when clicking on a button. The code is pretty simple but if you need a better understanding, you can browse the sample code on Github.

class MainActivityTest : AcceptanceTest<MainActivity>(MainActivity::class.java) {

  @Test
  fun shouldOpenHelloWorldScreen() {
    events.clickOnView(R.id.btn_hello_world)
    checkThat.nextOpenActivityIs(HelloWorldActivity::class.java)
  }

  @Test
  fun shouldDisplayAction() {
    events.clickOnView(R.id.fab)
    checkThat.viewIsVisibleAndContainsText(R.string.action)
  }
}

Running our test battery

There are neither problems nor especial configuration to run our tests from Android Studio/Intellij. I also added a couple of Gradle tasks in my root build.gradle file:

task runUnitTests(dependsOn: [':app:testDebugUnitTest']) {
  description 'Run all unit tests'
}

task runAcceptanceTests(dependsOn: [':app:connectedAndroidTest']) {
  description 'Run all acceptance tests.'
}

Just type this from the command line:

./gradlew runUnitTests
./gradlew runAcceptanceTests

Wrapping up

If at some point you were considering Kotlin, there are no longer excuses to use it in production. Tests are a good starting point to introduce it, plus this will help you to have a taste of what this language has to offer. Of course as an extra ball you get all the benefits mentioned in this article and a nice training that will prepare you and your team for the big change.

Repo

Resources