Android Kotlin – Testing

July 25, 2018, 8:56 a.m.
By: silverbaq
Android Test Kotlin Programming

I have been developing Android applications for some years now. But, one thing I have always ignored a lot, is writing tests for my application. Instead, I have been doing tedious debugging when an error acquires – Let’s face it, this is not a good idea in the long run.

Therefore, I have decided that I need to be a lot better at writing tests so that I create higher quality code with build in documentation.

 

When it comes to Android tests, it is worth noticing that there are three different types of tests:

  • Unit Tests: These are the “usual” type of test, for testing the model of the application.
  • Integration Tests: Similar to unit test, but is more android specific by testing the different parts of the system without adding the complex UI framework.
  • UI Test: For testing UI components (I will not be focusing on these for now)

Martin Fowlers: Test pyramid

https://medium.com/android-testing-daily/the-3-tiers-of-the-android-test-pyramid-c1211b359acd

Hands-on

I’ve created a simple “Todo” application to get my hands dirty. All it consists of, is an input field (EditText), a button and a list (ListView) to display what have been typed in by the user. When clicking an element on the list, it is supposed to be removed from the list. Very simple application, which should be a good starting point.

Unit Test

The applications model is more of less just a list of strings. 

Model code:

class Item(val id: Int, val value: String)

class Todo {
    val todoList: MutableList<Item> = mutableListOf()
    var lastId = 0

    fun addToList(value: String) {
        val item = Item(this.lastId++, value)
        todoList.add(item)
    }

    fun getSize(): Int {
        return todoList.size
    }

    fun removeFromList(id: Int) {
        val item = todoList.find { it.id.equals(id) }
        todoList.remove(item)
    }
}

Since I am more or less, just starting to take test seriously, I might not be able to create the best test scenarios. With the main idea of tests, being to find errors and bugs, so that they can be fixed. The tests I could come up with might overlook some important factors. The way I see it, your application will only be as functional as your test.

My unit tests are fairly simple:

import dk.im2b.androidtestdemo.model.Item
import dk.im2b.androidtestdemo.model.Todo
import org.hamcrest.Matchers.instanceOf
import org.junit.Test

import org.junit.Assert.*


class TodoUnitTest {
    @Test
    fun item_init() {
        val item = Item(0, "test")

        assertEquals("test", item.value)
        assertEquals(0, item.id)
    }

    @Test
    fun todo_init() {
        val todo = Todo()
        assertThat(todo.todoList, instanceOf<Any>(MutableList::class.java))
    }

    @Test
    fun todo_addsToList() {
        val todoList = Todo()
        val value = "test text"

        assertEquals(todoList.getSize(), 0)
        todoList.addToList(value)
        assertEquals(todoList.getSize(), 1)
    }

    @Test
    fun todo_removeFromList() {
        val todoList = Todo()
        val value = "test text"
        todoList.addToList(value)
        todoList.addToList(value)
        assertEquals(todoList.getSize(), 2)

        todoList.removeFromList(0)
        assertEquals(todoList.getSize(), 1)

        todoList.removeFromList(1)
        assertEquals(todoList.getSize(), 0)
    }

    @Test
    fun todo_removeFromList_sameId() {
        val todoList = Todo()
        val value = "test text"
        todoList.addToList(value)
        todoList.addToList(value)
        assertEquals(todoList.getSize(), 2)

        todoList.removeFromList(0)
        assertEquals(todoList.getSize(), 1)

        todoList.removeFromList(0)
        assertEquals(todoList.getSize(), 1)
    }

    @Test
    fun todo_removeFromList_wrongId() {
        val todoList = Todo()
        val value = "test text"
        todoList.addToList(value)
        assertEquals(todoList.getSize(), 1)

        todoList.removeFromList(2)
        assertEquals(todoList.getSize(), 1)
    }


}

 

From what I could understand, there is a very popular framework called “Mockito”, which creates mock objects for your unit test. Sounds very handy, so I’ll need to check that out.

Integration Test

Newly created android applications should already come with dependencies for doing integration tests. BUT, I never managed to get it to work. So, I tried out “Robolectric”, which just worked without any problems and is super cool in a lot of ways.

 

Activity:

 

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.widget.AdapterView
import dk.im2b.androidtestdemo.model.Item
import dk.im2b.androidtestdemo.model.Todo
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: MyAdapter
    private val todo = Todo()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Sets adapter
        val todos = mutableListOf<String>()
        adapter = MyAdapter(this,todo.todoList)

        listview.adapter = adapter
        listview.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, p2, p3 ->
            val item = adapter.getItem(p2) as Item
            removeFromTodo(item.id)
        }

        btnAdd.setOnClickListener {
            val text = etInput.text.toString()
            addToTodo(text)
        }
    }

    fun addToTodo(value: String) {
        if (value != "") {
            todo.addToList(value)
            adapter.notifyDataSetChanged()
        }
    }

    fun removeFromTodo(id: Int) {
        todo.removeFromList(id)
        adapter.notifyDataSetChanged()
    }
}

 

Test code:

 

import android.widget.Adapter
import android.widget.Button
import android.widget.EditText
import android.widget.ListView
import org.hamcrest.Matchers.instanceOf
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner



@RunWith(RobolectricTestRunner::class)
class MainActivityInstrumentedTest {
    val activity: MainActivity = Robolectric.setupActivity(MainActivity::class.java)


    @Test
    fun activity_isMainActivity(){
        assertThat(activity, instanceOf<Any>(MainActivity::class.java))
    }

    @Test
    fun activity_containsElements() {
        // Button is a button
        val button = activity.findViewById<Button>(R.id.btnAdd)
        assertThat(button, instanceOf<Any>(Button::class.java))

        // etInput is a EditText
        val input = activity.findViewById<EditText>(R.id.etInput)
        assertThat(input, instanceOf<Any>(EditText::class.java))

        // View contains a ListView
        val listview = activity.findViewById<ListView>(R.id.listview)
        assertThat(listview, instanceOf<Any>(ListView::class.java))
    }

    @Test
    fun listview_hasAdapter() {
        // Get Listview
        val listview: ListView = activity.findViewById(R.id.listview)
        val adapter = listview.adapter
        assertThat(adapter, instanceOf<Any>(Adapter::class.java))
    }

    @Test
    fun listview_addToView() {
        val listview: ListView = activity.findViewById(R.id.listview)

        assertEquals(0, listview.count)

        activity.addToTodo("test")
        assertEquals(1, listview.count)
    }

    @Test
    fun listview_cannotAddEmpty(){
        val listview: ListView = activity.findViewById(R.id.listview)

        assertEquals(0, listview.count)

        activity.addToTodo("")
        assertEquals(0, listview.count)
    }

    @Test
    fun listview_removeFromView(){
        val listview: ListView = activity.findViewById(R.id.listview)
        activity.addToTodo("test")
        activity.addToTodo("test")
        assertEquals(2, listview.count)

        activity.removeFromTodo(0)
        assertEquals(1, listview.count)

        activity.removeFromTodo(1)
        assertEquals(0, listview.count)
    }

    @Test
    fun view_canAddToList(){
        val editText = activity.findViewById<EditText>(R.id.etInput)
        val button = activity.findViewById<Button>(R.id.btnAdd)
        val adapter = activity.findViewById<ListView>(R.id.listview).adapter

        assertTrue(adapter.isEmpty)

        val testText = "Test 1"
        editText.setText(testText)
        button.performClick()

        assertTrue(!adapter.isEmpty)
    }

    @Test
    fun view_listviewClickRemoveItem_oneItem(){
        activity.addToTodo("test")
        val listview = activity.findViewById<ListView>(R.id.listview)

        val view = listview.getChildAt(0)
        listview.performItemClick(view,0,0)

        val count = listview.adapter.count
        assertEquals(0, count)
    }

    @Test
    fun view_listviewClickRemoveItem_multiItems(){
        activity.addToTodo("test")
        activity.addToTodo("test")
        activity.addToTodo("test")
        val listview = activity.findViewById<ListView>(R.id.listview)

        assertEquals(3,listview.adapter.count)

        var view = listview.getChildAt(1)
        listview.performItemClick(view,1,-1)
        assertEquals(2, listview.adapter.count)

        view = listview.getChildAt(1)
        listview.performItemClick(view,1,-1)
        assertEquals(1, listview.adapter.count)

        view = listview.getChildAt(0)
        listview.performItemClick(view,0,-1)
        assertEquals(0, listview.adapter.count)
    }

}

Again, they are very similar to how a unit test is structured, but tests more android specific things. (My tests here might not be best practice – Just wanted to stretch that out).

 

Final words

I did do a TDD approach, where I wrote a test first, which would fail. Wrote the minimal amount of code to make the test pass, then added a new test. By doing so, I was more aware of how my code could be tested when writing it – So I guess there is something about the part with, TDD making your code having a lower connection. I also easily see how this is documentation for your code – More more more…. :-D