Android Unit Testing with Mockito
In this Unit Testing with Mockito tutorial for Android, you will learn how to refactor an app in a way that makes it easy to write unit tests in Kotlin using Mockito. By Fernando Sproviero.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Unit Testing with Mockito
30 mins
- Testing pyramid
- Getting started
- Is this unit testable?
- Logic in the Activities
- Activity doing everything
- Adapter doing too much logic
- Conclusion
- Model-View-Presenter
- Refactoring to MVP
- Creating your first Presenter
- Implementing your first View
- Creating the SearchResults presenter
- Implementing SearchResultsPresenter.View
- MVP Refactor done!
- Using Mockito
- Setup Mockito dependencies
- State & Behavior verification
- Mockito main features
- Search unit tests
- Refactoring the unit test
- Verify a method is never called
- Search results tests
- Kotlin default final classes/methods
- Repository refactor
- Setup Search results tests
- Stub a callback
- Verify state
- RecipeRepository tests
- Mock maker inline
- Spy the repository implementation
- Where To Go From Here?
Conclusion
Because these are so embedded into the Android activities/adapter, they are not efficiently unit testable. A refactor must be done!
Model-View-Presenter
You’ll refactor the project to the Model-View-Presenter structure. This will ease the creation of unit tests.
-
Model:
All your data classes, API connectors, databases. -
View:
Activities, Fragments and any Android Views. It’s responsible for showing the data and propagating the user’s UI actions to the corresponding presenter methods. -
Presenter:
Knows about the model and the view. Publishes methods that will be called by the view. These methods usually involve fetching data, manipulating it, and deciding what to show in the view.
Refactoring to MVP
Creating your first Presenter
First, you’ll refactor the SearchActivity to MVP.
Therefore, create a new class by right-clicking on the com.raywenderlich.ingredisearch
package and choosing New-Kotlin File/Class. Name the class SearchPresenter and add the following:
class SearchPresenter { // 1 private var view: View? = null // 2 fun attachView(view: View) { this.view = view } // 3 fun detachView() { this.view = null } // 4 fun search(query: String) { // 5 if (query.trim().isBlank()) { view?.showQueryRequiredMessage() } else { view?.showSearchResults(query) } } // 6 interface View { fun showQueryRequiredMessage() fun showSearchResults(query: String) } }
- The presenter knows about the view, so it has to hold a reference to it.
- When the view is created, it must attach to the presenter.
- You must detach from the presenter when the view is destroyed.
- This presenter exposes the search method.
- If the query is blank then the view has to show a “query required” message. If it’s not blank, it’ll show the results.
- A View interface that your activity will have to conform to.
Implementing your first View
Now, open SearchActivity and modify it to the following:
// 1 class SearchActivity : ChildActivity(), SearchPresenter.View { private val presenter: SearchPresenter = SearchPresenter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_search) // 2 presenter.attachView(this) searchButton.setOnClickListener { val query = ingredients.text.toString() // 3 presenter.search(query) } } override fun onDestroy() { // 4 presenter.detachView() super.onDestroy() } // 5 override fun showQueryRequiredMessage() { // Hide keyboard val view = this.currentFocus if (view != null) { val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(view.windowToken, 0) } Snackbar.make(searchButton, getString(R.string.search_query_required), Snackbar .LENGTH_LONG).show() } // 6 override fun showSearchResults(query: String) { startActivity(searchResultsIntent(query)) } }
- Conform to the SearchPresenter.View interface by implementing 5 and 6.
- Create a new instance of the presenter and attach the view.
- Whenever the user clicks the search button, instead of the activity doing any logic, just call the search method of the presenter.
- When the view is destroyed you must tell the presenter to detach the view.
- Implement showQueryRequiredMessage required by the SearchPresenter.View interface.
- SearchPresenter.View also requires you to implement showSearchResults.
Creating the SearchResults presenter
Create a SearchResultsPresenter class.
// 1 class SearchResultsPresenter(val repository: RecipeRepository) { private var view: SearchResultsPresenter.View? = null private var recipes: List<Recipe>? = null // 2 fun attachView(view: SearchResultsPresenter.View) { this.view = view } fun detachView() { this.view = null } // 3 interface View { fun showLoading() fun showRecipes(recipes: List<Recipe>) fun showEmptyRecipes() fun showError() fun refreshFavoriteStatus(recipeIndex: Int) } }
- This presenter will make the API request therefore it has RecipeRepository as dependency.
- You also need to attach/detach.
- A View interface that your activity will have to conform to.
You may have noticed that you’re repeating the attach/detach logic here, so let’s create a BasePresenter:
abstract class BasePresenter<V> { protected var view: V? = null fun attachView(view: V) { this.view = view } fun detachView() { this.view = null } }
Now extend from this class, remove the view property and the attach/detach methods:
class SearchResultsPresenter(private val repository: RecipeRepository) : BasePresenter<SearchResultsPresenter.View>() { private var recipes: List<Recipe>? = null interface View { fun showLoading() fun showRecipes(recipes: List<Recipe>) fun showEmptyRecipes() fun showError() fun refreshFavoriteStatus(recipeIndex: Int) } }
Add the following method:
// 1 fun search(query: String) { view?.showLoading() // 2 repository.getRecipes(query, object : RecipeRepository.RepositoryCallback<List<Recipe>> { // 3 override fun onSuccess(recipes: List<Recipe>?) { this@SearchResultsPresenter.recipes = recipes if (recipes != null && recipes.isNotEmpty()) { view?.showRecipes(recipes) } else { view?.showEmptyRecipes() } } // 4 override fun onError() { view?.showError() } }) }
- This presenter exposes the search method.
- Call the repository to get recipes asynchronously.
- If the call is successful show the recipes (or empty if there are none).
- Whenever there is an error with the call, show the error.
Add the following extra methods:
// 1 fun addFavorite(recipe: Recipe) { // 2 recipe.isFavorited = true // 3 repository.addFavorite(recipe) // 4 val recipeIndex = recipes?.indexOf(recipe) if (recipeIndex != null) { view?.refreshFavoriteStatus(recipeIndex) } } // 5 fun removeFavorite(recipe: Recipe) { repository.removeFavorite(recipe) recipe.isFavorited = false val recipeIndex = recipes?.indexOf(recipe) if (recipeIndex != null) { view?.refreshFavoriteStatus(recipeIndex) } }
- Expose the addFavorite method.
- Alter the state of the model.
- Call the repository to save the favorite.
- Tell the view to refresh with the favorited status.
- Analogously, expose the removeFavorite method.
Implementing SearchResultsPresenter.View
Now, open SearchResultsActivity and modify it to the following:
// 1
class SearchResultsActivity : ChildActivity(), SearchResultsPresenter.View {
private val presenter: SearchResultsPresenter by lazy {SearchResultsPresenter(RecipeRepository.getRepository(this))}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.subtitle = query
// 2
presenter.attachView(this)
// 3
presenter.search(query)
retry.setOnClickListener { presenter.search(query) }
}
}
- Conform to the SearchResultsPresenter.View interface.
- Instantiate the presenter with the repository and attach the view.
- Whenever the user enters this screen, instead of the activity doing any logic, just call the search method of the presenter.
- When the view is destroyed you must tell the presenter to detach the view.
Implement the SearchResultsPresenter.View interface:
override fun showEmptyRecipes() {
loadingContainer.visibility = View.GONE
errorContainer.visibility = View.GONE
list.visibility = View.VISIBLE
noresultsContainer.visibility = View.VISIBLE
}
override fun showRecipes(recipes: List<Recipe>) {
loadingContainer.visibility = View.GONE
errorContainer.visibility = View.GONE
list.visibility = View.VISIBLE
noresultsContainer.visibility = View.GONE
setupRecipeList(recipes)
}
override fun showLoading() {
loadingContainer.visibility = View.VISIBLE
errorContainer.visibility = View.GONE
list.visibility = View.GONE
noresultsContainer.visibility = View.GONE
}
override fun showError() {
loadingContainer.visibility = View.GONE
errorContainer.visibility = View.VISIBLE
list.visibility = View.GONE
noresultsContainer.visibility = View.GONE
}
override fun refreshFavoriteStatus(recipeIndex: Int) {
list.adapter.notifyItemChanged(recipeIndex)
}
Implement the missing method:
private fun setupRecipeList(recipes: List<Recipe>) {
list.layoutManager = LinearLayoutManager(this)
list.adapter = RecipeAdapter(recipes, object : RecipeAdapter.Listener {
override fun onClickItem(recipe: Recipe) {
startActivity(recipeIntent(recipe.sourceUrl))
}
override fun onAddFavorite(recipe: Recipe) {
// 1
presenter.addFavorite(recipe)
}
override fun onRemoveFavorite(recipe: Recipe) {
// 2
presenter.removeFavorite(recipe)
}
})
}
- When adding a favorite, now the adapter listener just calls the presenter’s addFavorite method.
- Also, when the user wants to remove a favorite, just call the presenter’s removeFavorite method.