Tips on Unit Testing In Android with Kotlin

Indri Yunita
Ruangguru Engineering
6 min readAug 23, 2019

Hello! Today I’d like to talk about some tips on unit testing in Android with Kotlin. Some of them may have more to do with unit testing in general, which for the most part are related to how to create such “flow” and cutting down on unnecessary things, a.k.a productivity tips.

As for the unit testing framework, JUnit 5 has been out for a while now and there have been numerous talks about how you should adopt it, but JUnit 4 has been the standard for the longest time. In this article, I’m using JUnit 4.

Setup

A little setup in app-level build.gradle:

dependencies {
testImplementation ‘junit:junit:4.12’
testImplementation ‘org.mockito:mockito-core:2.25.0’
}

The junit part by default is there when you create a new project, but we need to add mockito dependency by ourselves. Now, we could do unit testing without Mockito. It’s just a framework to help us creating mocks as replacements for dependencies in our project. Mock itself is just one kind of replacement among several others. Which brings us to the next part.

Know Your Test Doubles

A unit test should test functionality in isolation. Any external dependencies not related with the system under test should be replaced and conditioned to create a “controlled environment”. In the world of unit testing, this replacement has an umbrella term: test double. Now there has been some common confusion around the term. Some people say “mock” when they mean “test double” in general. Meanwhile, test double comprises:

  1. Dummy object, when it’s passed around but its methods are never called. It doesn’t have behavior.

2. Fake, is an object that has working implementations but not the same as production ones, usually, are simplified. One common example is when you implement a Repository using in-memory implementation.

3. Stub, is an object that has a partial implementation to an interface or class. It is there to satisfy some limited needs and usually, the implementation or behavior is predetermined.

4. Mock, is the kind of test double you have to set up with expectations.

Further reading on this subject:

Test Method Naming

Best practice for the naming test method in Kotlin is to put method names in between backticks and use space between words. For example:

@Test
fun `update username success user is cached`() { ... }

This, arguably, improves the readability. But, I personally like the Java tradition better, that is methodName_stateUnderTest_expectedBehavior, because there is clear distinction between the roles of elements in the name. The above test method then becomes:

@Test
fun updateUsername_success_userCached() { ... }

Mockito Kotlin

Look at the test code below:

import org.mockito.ArgumentCaptor...@Test
fun updateUsername_success_userCached() {
...
ArgumentCaptor.forClass(User::class.java).apply {

SUT.updateUsername(userId = USER_ID, username = USER_NAME)

verify(usersCacheMock).cacheUser(capture())
val cachedUser = firstValue
assertThat(cachedUser.userId, `is`(USER_ID))
assertThat(cachedUser.username, `is`(USER_NAME))
}
}

When you run the test, it throws error “java.lang.IllegalStateException: ac.capture() must not be null”. It’s because I have specified “user” in “cacheUser” to be non-nullable, while “ac.capture()” returns null. To tackle this, some fine guy named Niek Haarman created a helper library, Mockito-Kotlin. To add the dependency, add this:

testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"

Then you just have to replace Mockito’s default implementation for ArgumentCaptorwith Mockito-Kotlin’s. The above test method then becomes:

import com.nhaarman.mockitokotlin2.argumentCaptor...@Test
fun updateUsername_success_userCached() {
...
argumentCaptor<User>().apply {

}
}

Et voila! The test runs without error.

State All Test Cases Before Writing Tests

It’s a good habit to write all possible test cases that come to mind before you start writing tests. For example, the requirements for fetching user are below:

1) If the user with given user ID is not in the cache then it should be fetched from the server.
2) If the user fetched from the server then it should be stored in the cache before returning to the caller.
3) If the user is in the cache then cached record should be returned without polling the server.

Then we translate the requirements above into our own interpretation:

// 1. if user is not in cache, remote is called (Req 1)// 2. if fetch remote succeed, user from remote is returned (Req 2)
// 3. if fetch remote succeed, user is cached (Req 2)
// 4. if fetch remote fail, user is not cached (Req 2)
// 5. if user is in cache, remote is not called (Req 3)
// 6. if user is in cache, user from cache is returned (Req 3)

Then, after that, we can start turning each of them into the test method, and just clear the comments afterward. This is a good habit because you will have an exhaustive list of things to test from the beginning, instead of losing track after writing a couple of test methods due to context switching.

Comment and Give Clear Distinctions Between Given/When/Then (Arrange/Act/Assert)

In general, unit test methods consist of 3 parts: conditioning, action, and verifying the result. This is often called Given-When-Then, or, I like this one better: Arrange-Act-Assert. Consider turning the first statement above (if user is not in cache, remote is called) into a test, then it will be like this:

@Test
fun fetchUser_userNotInCache_remoteFetchIsCalled() {
// Arrange
`when`(cacheMock.getUser(ArgumentMatchers.anyString()))
.thenReturn(null)
`when`(endpointMock.fetchUser(ArgumentMatchers.anyString()))
.thenReturn(EndpointResult(EndpointStatus.SUCCESS, USER_ID, USERNAME))

// Act
SUT.fetchUser(USER_ID)

// Assert
verify(endpointMock).fetchUser(USER_ID)
}

Put labels for each section and spaces between them. It makes it much more readable.

Extract the Code in “Arrange” Section into Separate Helper Methods

Chances are, you are going to reuse the “arrange” part in other methods. For example, “if the user is in cache” is called in two test methods. So, extract them into separate methods. This also makes our unit test cleaner.

// helper methods
fun userInCache() {
`when`(cacheMock.getUser(ArgumentMatchers.anyString()))
.thenAnswer {
val userId = it.arguments[0] as String
User(userId, USERNAME)
}
}
fun userNotInCache() {
`when`(cacheMock.getUser(ArgumentMatchers.anyString()))
.thenReturn(null)
}

Use Live Template

Using Live Template, you can automatically generate comments for Arrange, Act, and Assert and give spaces between them. To do this, go to Preferences (Cmd+,) > Editor > Live Templates > Add (Cmd+N) > 1. Live Template. Give “Abbreviation”, which in my case is “test”, because: (1) it’s short enough, and (2) it’s self-explanatory. And fill out some short description.

Then put the template part:

@org.junit.Test
fun $NAME$() {
// Arrange


// Act
$END$


// Assert

}

$END$ is a special variable that specifies where your cursor is going to end up. I put this under the “Act” comment because this part will never be empty (while you could do the test without the “Arrange” part at all).

Use whenever instead of `when`

Now that we are using Mockito-Kotlin, there is this function, whenever() which is an alias for Mockito.when() (that we have to escape with backticks because when is a reserved word in Kotlin). Just prettier.

Bonus: Use assertThat instead of assertEquals

Because of this:

https://stackoverflow.com/questions/1701113/why-should-i-use-hamcrest-matcher-and-assertthat-instead-of-traditional-assert

By the way, I’d like to give credit to Vasiliy Zukanov for a lot of what I wrote here. His “Android Unit Testing and Test Driven Development” course on Udemy is totally exemplary. That’s it for now. I hope you enjoyed the post :]

--

--

Indri Yunita
Ruangguru Engineering

A work in progress; I’m always changing. My aspiration is to be the all-singing, all-dancing crap of the world, unsubjugated by this relentless world.