How to write a testable code
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 existingids
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 thenadd items to adapter
.getting menus with
pageNumber=0
thennot
call Api.getting menus given pageNumber and
Successful
response thenstop loading of swipe to refresh
.getting menus given pageNumber and
UnSuccessful
response thenstop loading of swipe to refresh
.getting menus given pageNumber and
UnSuccessful
response thenshow 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)
}
}