How to write a testable code

8 minute read

Published:

Why We Need Unit Testing!

let’s see this snippet of code

fun invalidateCache(users: List<User>) {
	val userIds=users.map{it.userId}
    getDBUsers().forEach { user ->
        if (user.userId == null) {
         	//invalid data
            DatabaseManager.deleteAllUsers()
            return
        } else (!usersIds.contains(user.userId))
        DatabaseManager.deleteUserById(it.userId)
    }
}

can you translate this code? oh yes

  • it checks for id if it’s null then we need to delete all from our DB and break.
  • and if not then check if this id doesn’t exist in our existing ids then delete it from our DB.

but this is not what happend here!!

what happened is when it’s not null all ids get deleted from our DB!! so let’s debug and see the reason.. after a long time i discoverd that the issue is in the 4th line of code which is:

else (!usersIds.contains(user.userId))

what is the use of this line? it doesn’t check for anything, it’s just an else but here an else if is needed instead like this:

else if (!usersIds.contains(user.userId))

it was a production code BTW, so what if we wrote a unit test for this couple lines of code!

What do you need to write a good unit test?

  • You do not need to learn any framework to start Unit Testing for mocking or something else you can do this by yourself.

  • Your code must be designed to be testable so that’s why we have Architectural Design Patterns, and you can read about Clean Architecture from here.

What is unit test?

a unit test is a method which designed to test the behaviour of actual method using assertion.

Let’s take an example using method for adding two numbers

fun add (number1:Int, number2:Int) = number1 + number2

the unit test should follow triple A (AAA) rule which is:

1- A-rrange: this is the first step of a unit test application. Here we will arrange the test, in other words we will do the necessary setup of the test. for our example

val number1 = 5
val number2 = 6

2- A-ct: this is the middle step of a unit test application. In this step we will execute the test.

val sum = add (number1, number2) 

3- A-ssert: this is the last step of a unit test application. In this step we will check and verify the actual results with expected results using assertions. if not true it’ll fail and you will know that you need to edit your code.

Assert (sum == 11) //it must be true

each unit test should have only one assertion.

and now let’s dig deep and learn how to design a testable project

let’s imagine this activity

class UnTestableMenusListActivity : AppCompatActivity() 
{
    private lateinit var adapter: MenuAdapter
    var lastPageNo: Int = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_menus_list)
        setupRecycler()
        getMenus()
    }

    private fun setupRecycler() {...}

    private fun getMenus() {
    	swipeRefresh.isRefreshing = true

        RetrofitClient.client.create(MenusApi::class.java)
                .getItems(lastPageNo).enqueue(object : Callback<MenuList> {
                    override fun onResponse(call: Call<MenuList>, response: Response<MenuList>) {
                    	swipeRefresh.isRefreshing = false
                        response.body()?.let {
                            val menusWithPage = mapOf(lastPageNo to response.body()!!.items)
                            adapter.addAll(it.items, menusWithPage.keys.first())
                            lastPageNo++
                        }
                    }

                    override fun onFailure(call: Call<MenuList>, t: Throwable) {
                    	swipeRefresh.isRefreshing = false
                        tvNetworkError.visibility = View.VISIBLE
                    }
                  })
    }
}

in this code we have an activity that contains a recycler view to view a list of restaurant menus from an api using getMenus() method.

Let’s see how to test this method. First we need to write down our test cases like this:

  • getting menus given pageNumber then show loading.

  • getting menus given pageNumber and Successful response then add items to adapter.

  • getting menus with pageNumber=0 then not call Api.

  • getting menus given pageNumber and Successful response then stop loading of swipe to refresh.

  • getting menus given pageNumber and UnSuccessful response then stop loading of swipe to refresh.

  • getting menus given pageNumber and UnSuccessful response then show error of text view.

now we’re gonna see how to write our first test case for this method, but wait you should take care of the following:

this method is inside an activity (Android Component) so it requires mocking Adnroid Component which is not the right thing about unit test as uncle bob mentioned View Class is a stupid class it shouldn’t be tested, and shouldn’t have any logic in it too.

the second thing you need to take care of is how we can control the response of Retrofit as it’s inside the method itself!

Architecture Redesign

let’s redesign our method to be testable for our example we will use Clean Architecture, MVVM as an Architecture Design Pattern With Android Architecture Components.

so our activity will be

class MenusListActivity : AppCompatActivity(){

    private lateinit var adapter: MenuAdapter
    lateinit var viewModel: MenuViewModel
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_menus_list)
        viewModel = ViewModelProviders.of(this).get(MenuViewModel::class.java)
        setupRecycler()
        initObservables()
    }

    private fun setupRecycler() {...}

    private fun initObservables() {
        viewModel.loadMore.observe(this, Observer { visibility ->
            progresssLoadMore.visibility = visibility
        })
        viewModel.networkError.observe(this, Observer { visibility ->
            tvNetworkError.visibility = visibility
        })
        viewModel.loading.observe(this, Observer { isLoading ->
            swipeRefresh.isRefreshing = isLoading
        })

        viewModel.menus.observe(this, Observer { menusWithPage ->
            menusWithPage[menusWithPage.keys.first()]?.let {
                adapter.addAll(it, menusWithPage.keys.first())
            }
        })
    }
}

and our ViewModel

class MenuViewModel(private val getMenusUseCase: GetMenusUseCase = GetMenusUseCase()) : ViewModel(), Callback<MenuList> {


    var loadMore = SingleLiveEvent<Int>()
    var networkError = SingleLiveEvent<Int>()
    var loading = SingleLiveEvent<Boolean>()
    var menus = MutableLiveData<Map<Int, List<Menu>>>()

    private var lastPageNo: Int = 0

    init {
        getMenus()
    }

    fun getMenus(pageNo: Int = 1) {
        if (pageNo == 1) loading.value = true else loadMore.value = View.VISIBLE
        lastPageNo = pageNo
        getMenusUseCase.getAllMenus(pageNo).enqueue(this)
    }

    override fun onResponse(call: Call<MenuList>?, response: Response<MenuList>) {
        loading.value = false
        loadMore.value = View.GONE
        networkError.value = View.GONE

        if (response.isSuccessful)
            menus.value = mapOf(lastPageNo to response.body()!!.items)
    }

    override fun onFailure(call: Call<MenuList>?, t: Throwable?) {
        loading.value = false
        loadMore.value = View.GONE
        networkError.value = View.VISIBLE
    }
}

so let’s think of how to test the method called getMenus consedring the points above:

this method is not inside an activity (Android Component) so it doesn’t require mocking Adnroid Component.

the second thing you can control is the response of Retrofit as it’s wrapped using use cases and repository.

so our use-case

 class GetMenusUseCase {
    fun getAllMenus(pageNo: Int, menusRepository: MenusRepository = MenusRepository()) = menusRepository.getAllMenus(pageNo)
}

and our repo

private val defaultRemoteDataSource by lazy { MenusRemoteDataSource() }

class MenusRepository(private val remoteDataSource: MenusRemoteDataSource = defaultRemoteDataSource) : MenusDataSource {

    override fun getAllMenus(pageNo: Int): Call<MenuList> = remoteDataSource.getAllMenus(pageNo)
}

and here we can test these classes easily as they don’t depend on android framework.

Stubbing VS. Mocking

the purpose of both is to eliminate testing all the dependencies of a class or function so your tests are more focused and simpler in what they are trying to prove.

you can mock using any framwork like mockito or something else but here we’re gonna stubbing by ourselves.

and our stubbing network will be like this

class RetrofitStubbing(val success: Boolean = true, val enqueue: Boolean = true) : MenusApi {
    override fun getItems(pageNumber: Int): Call<MenuList> {
        return object : Call<MenuList> {
            override fun request(): Request = Request.Builder().build()

            override fun enqueue(callback: Callback<MenuList>) {
                if (enqueue) {
                    if (success)
                        callback.onResponse(this, Response.success(MenuList(listOf(Menu(1, "menu", "", "")))))
                    else
                        callback.onFailure(this, Throwable("error"))
                }
            }

            override fun isExecuted(): Boolean = false

            override fun clone(): Call<MenuList> = this

            override fun isCanceled(): Boolean = false

            override fun cancel() {}

            override fun execute(): Response<MenuList> =
                    if (success) {
                        Response.success(MenuList(listOf(Menu(1, "menu", "", ""))))
                    } else
                        Response.error(500, ResponseBody.create(MediaType.get("application/json"), "error"))
        }
    }
}

here we have two inputs:

  • success: what you need is a successfull response or failure.
  • enqueue: do you need to execute the request or just checking something once request is built as in our case we’re gonna check for loading start before request is back.

Writing unit tests

now we’re ready to write our unit tests so let’s take a look at it

class MenuViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun `getting menus given pageNumber then show loading`() {

        //arrange
        val menusRepository = MenusRepository(MenusRemoteDataSource(RetrofitStubbing(enqueue = false)))
        val useCase = GetMenusUseCase(menusRepository)
        val menuViewModel = MenuViewModel(useCase)

        //act
        menuViewModel.getMenus()

        //assert
        assert(menuViewModel.loading.value == true)
    }

    @Test
    fun `getting menus given pageNumber and Successful response then add items to adapter`() {
        //arrange
        val menusRepository = MenusRepository(MenusRemoteDataSource(RetrofitStubbing()))
        val useCase = GetMenusUseCase(menusRepository)
        val menuViewModel = MenuViewModel(useCase)

        //act
        menuViewModel.getMenus()

        //assert
        assert(menuViewModel.menus.value?.size == 1)
    }

    @Test
    fun `getting menus given pageNumber and UnSuccessful response then show error of text view`() {

        //arrange
        val menusRepository = MenusRepository(MenusRemoteDataSource(RetrofitStubbing(false)))
        val useCase = GetMenusUseCase(menusRepository)
        val menuViewModel = MenuViewModel(useCase)

        //act
        menuViewModel.getMenus()

        //assert
        assert(menuViewModel.networkError.value == View.VISIBLE)

    }


}

Note: You’ll find all the code for this project in the following Github