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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

Note: If you already know about MVP you can skip to the next section where the project is already refactored to MVP and ready to write unit tests.


Also called just MVP

model view presenter


Also called just MVP

  • 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)
  }
}
  1. The presenter knows about the view, so it has to hold a reference to it.
  2. When the view is created, it must attach to the presenter.
  3. You must detach from the presenter when the view is destroyed.
  4. This presenter exposes the search method.
  5. 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.
  6. 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))
  }
}
  1. Conform to the SearchPresenter.View interface by implementing 5 and 6.
  2. Create a new instance of the presenter and attach the view.
  3. Whenever the user clicks the search button, instead of the activity doing any logic, just call the search method of the presenter.
  4. When the view is destroyed you must tell the presenter to detach the view.
  5. Implement showQueryRequiredMessage required by the SearchPresenter.View interface.
  6. 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)
  }
}
  1. This presenter will make the API request therefore it has RecipeRepository as dependency.
  2. You also need to attach/detach.
  3. 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()
      }
    })
  }
  1. This presenter exposes the search method.
  2. Call the repository to get recipes asynchronously.
  3. If the call is successful show the recipes (or empty if there are none).
  4. 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)
    }
  }
  1. Expose the addFavorite method.
  2. Alter the state of the model.
  3. Call the repository to save the favorite.
  4. Tell the view to refresh with the favorited status.
  5. 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) }
  }
}
  1. Conform to the SearchResultsPresenter.View interface.
  2. Instantiate the presenter with the repository and attach the view.
  3. Whenever the user enters this screen, instead of the activity doing any logic, just call the search method of the presenter.
  4. 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)
      }
    })
  }
  1. When adding a favorite, now the adapter listener just calls the presenter’s addFavorite method.
  2. Also, when the user wants to remove a favorite, just call the presenter’s removeFavorite method.