TL; DR: This article is aimed at beginners of unit testing. It starts with a brief introduction to what unit testing is, why to write unit tests, a discussion of what code is suitable for unit testing in Android projects, and a simple example of how to write your first Android unit test (Kotlin code).

What are unit tests

Unit testing is the test work of verifying the correctness of the smallest unit of the program. A program unit is the smallest testable part of an application. A unit may be a single program, class, object, method, and so on. – Wikipedia

Why do unit tests

Code without tests is unreliable. Lu xun –

  • Verify that the code is correct and increase your confidence in the code

The most immediate benefit. In the absence of unit tests, the usual way to test yourself is to run through the program, simply construct the main branch scenarios, and if it passes, it’s OK to submit to QA students. But sometimes branches are undetectable or difficult to construct on their own, and this can only be covered by manual testing by QA students, and if they fail, heaven help them. Unit testing makes it easier to construct test scenarios and gives us more confidence in the code that passes the test

  • Maintain or improve product quality without QA involvement

In plain English, refactoring is safe. QA students always talk about refactoring and get nervous when refactoring legacy code for fear of changing old logic or accidentally affecting other modules. With unit tests, we can be more aggressive in refactoring, just run the test pass after refactoring (suitable for small refactoring, large refactoring may require rewriting unit tests).

  • Deepen business understanding

When designing test cases, you need to consider the various scenarios in the business to help you understand the business beyond the code

  • To help you write better code

Unit testing requires high cohesion and low coupling of the code being tested, so you need to think about how to write tests when you write business code, or conversely, writing test cases first will allow you to write more structured code

What does unit testing cost? Of course, writing and maintaining test cases takes a certain amount of time and energy. When the project schedule is under great pressure, many people are unwilling to spend time writing tests. This is a trade-off between not writing and losing all the benefits mentioned above, or having time to catch up later and missing the best time to write the test.

Android Unit Testing

By default, Android projects create two test directories, respectively SRC /test and SRC /androidTest. The former is the unit test directory, and the latter is the Instrumentation test directory that depends on the Android framework. State testing also has difference, the former is testImplementation latter is androidTestImplementation, today we talk about the former, also known as the Local Unit Test, meaning that is not depend on the Android’s machine or simulator, Unit tests that can be run directly on the native JVM.

Unit testing for Android is not that different from normal Java projects, and the first thing to focus on is identifying which classes or methods need to be tested.

An important feature of a good unit test is that it runs fast, usually in milliseconds, and code that relies on the Android framework runs on emulators or real machines (but not always), which is inevitably much slower. So when we do Android unit testing, we try to avoid having the code under test have any dependence on the Android framework. In this case, generally good code for unit testing is:

  1. Presenter in MVP structure or ViewModel in MVVM structure
  2. Helper or Utils utility classes
  3. Common base modules, such as network libraries, databases, etc

If the code in your project is highly coupled to the Android framework, you may have to refactor the object code before writing the test code. How to refactor is beyond the scope of this article; explore on your own.

Write your first Android unit test

SETUP

Android unit testing uses JUnit testing framework + Mockito Mock class library + Mockito-Kotlin extension library. You need to declare test dependencies in build.gradle. The following example code corresponds to the following dependencies.

testImplementation 'junit: junit: 4.12'
testImplementation 'org. Mockito: mockito - core: 2.19.0'
testImplementation 'com. Nhaarman. Mockitokotlin2: mockito - kotlin: 2.1.0'
Copy the code

The details of what each library is used for are explained in the following code.

The target code

Here is a simple EXAMPLE of Presenter from MVP to illustrate how to write unit tests. The following test code from here is a recipe search results display page.

class SearchResultsPresenter(private val repository: RecipeRepository) :
    BasePresenter<SearchResultsPresenter.View>() {
  private var recipes: List<Recipe>? = null

  fun search(query: String){ view? .showLoading() repository.getRecipes(query,object : RecipeRepository.RepositoryCallback<List<Recipe>> {
      override fun onSuccess(recipes: List<Recipe>? {
        this@SearchResultsPresenter.recipes = recipes
        if(recipes ! =null&& recipes.isNotEmpty()) { view? .showRecipes(recipes) }else{ view? .showEmptyRecipes() } }override fun onError(a){ view? .showError() } }) }fun addFavorite(recipe: Recipe) {
    recipe.isFavorited = true

    repository.addFavorite(recipe)

    valrecipeIndex = recipes? .indexOf(recipe)if(recipeIndex ! =null) { view? .refreshFavoriteStatus(recipeIndex) } }fun removeFavorite(recipe: Recipe) {
    repository.removeFavorite(recipe)
    recipe.isFavorited = false
    valrecipeIndex = recipes? .indexOf(recipe)if(recipeIndex ! =null) { view? .refreshFavoriteStatus(recipeIndex) } }interface View {
    fun showLoading(a)
    fun showRecipes(recipes: List<Recipe>)
    fun showEmptyRecipes(a)
    fun showError(a)
    fun refreshFavoriteStatus(recipeIndex: Int)}}Copy the code

Take a quick look at the code.

First of all, the Presenter class contains an internal View class that defines the interface methods that a View should implement in MVP, including displaying load status, displaying recipe lists, displaying empty pages, displaying error pages, and refreshing favorites.

Its constructor takes a RecipeRepository object, so let’s look at the definition of RecipeRepository.

interface RecipeRepository {
  fun addFavorite(item: Recipe)
  fun removeFavorite(item: Recipe)
  fun getFavoriteRecipes(a): List<Recipe>
  fun getRecipes(query: String, callback: RepositoryCallback<List<Recipe> >)
}

interface RepositoryCallback<in T> {
  fun onSuccess(t: T?).
  fun onError(a)
}
Copy the code

As the name suggests, it’s a recipe data store that defines a set of data fetching and updating interfaces. It doesn’t matter where you get data from — local files, databases, networks, etc. This is also the embodiment of the dependency reversal principle.

This Presenter in turn inherits BasePresenter, which is an abstract class that defines two methods, attachView() and detachView(), as well as a field view.

abstract class BasePresenter<V> {
  protected var view: V? = null

  fun attachView(view: V) {
    this.view = view
  }

  fun detachView(a) {
    this.view = null}}Copy the code

Back to SearchResultsPresenter itself, this class has three main methods. The first one takes a string, calls the Repository method to retrieve the search results, and calls the View methods based on the results. The second, addFavorite(), takes a recipe object, sets it to favorite, calls repository to update to the repository, and finally calls the View method to refresh the UI; The third method, removeFavorite(), is the opposite of the previous method. Base class methods are out of our testing scope, so don’t worry about them.

These three methods are definitely the goal of our unit testing, moving on to how to write test code.

Creating a test class

First locate the class we want to Test, use the shortcut CMD + N (Generate), and select Test. A pop-up window will pop up, leading us to create a corresponding Test class. The name of the class we want to Test is usually the suffix + Test. Remember to place the location in the SRC /test directory (you can also manually locate the location and create a new file, but this is much slower).

Write test code

Conduct verification

Start by adding the following code

class SearchResultsPresenterTests {

  private lateinit var repository: RecipeRepository
  private lateinit var presenter: SearchResultsPresenter
  private lateinit var view: SearchResultsPresenter.View

  @Before
  fun setup(a) {
    repository = mock()
    view = mock()
    presenter = SearchResultsPresenter(repository)
    presenter.attachView(view)
  }
Copy the code

To clarify, there are two areas of code that may be unfamiliar:

  1. @ Before annotations

This annotation is part of the Junit testing framework, and every test case in the current test class calls the @Before annotation method first, so it can be used to do some common setup operations. In this case, we are testing Presenter, so we create an instance of Presenter and configure external objects such as View/Repository to interact with Presenter. Corresponding to Before, there is an @after annotation that marks a method to clean up After each use case, or omit it if not needed.

  1. The mock () method

This method is provided by the Mockito-Kotlin library, which is a wrapper library that calls the Mockito library. This library can be used to forge some stable dependent classes, so that our unit test results will not be unpredictable due to unstable dependencies. The View and Repo classes that interact with Presenter have abstract interfaces. We don’t want to test the View and Repo classes. A mock() method can be used to create a mock class (here mock() is a generic method that uses Kotlin’s type inference feature). Mock classes can be used to check whether methods are called, how many times they are called, and in what order they are called, and so on.

Next we add our first test case to verify that the showLoading() method of View is called after calling the Presenter search() method.

@Test
fun search_callsShowLoading(a) {
    presenter.search("eggs")
    verify(view).showLoading()
}
Copy the code

The presenter search method is called, of course. Then we call a verify method that accepts a Mock object and verifies that the Mock object’s showLoading() method has been called! It’s very simple. To the left of the method declaration, there is a run button that you can click to execute the test case (Ctrl + Shift + R).

Let’s write a more complex test case, this time verifying that repo’s getRecipes() method is called after the search() call and view’s showRecipes() method is called when the callback returns.

@Test
fun search_succeed_callShowRecipes(a) {
    val recipe = Recipe("id"."title"."imageUrl"."sourceUrl".false)
    val recipes = listOf(recipe)
    doAnswer {
        val callback: RepositoryCallback<List<Recipe>> = it.getArgument(1)
        callback.onSuccess(recipes)
    }.whenever(repository).getRecipes(eq("eggs"), any())

    presenter.search("eggs")

    verify(repository).getRecipes(eq("eggs"), any())
    verify(view).showRecipes(eq(recipes))
}
Copy the code

Oh, this method is a lot of code, but don’t freak out, it’s easy to understand. First we created recipes objects that returned recipes for repo searches. Here we used a new method, doAnswer{}.whenever().getrecipes (), We do something when we call the Mock getRecipes() method. In the body of the doAnswer{} method, we take the callback object and execute the onSuccess() callback. Return the search results we’ve constructed (a process called Stubbing, which translates to Stubbing). Ok, so now that we have the test prerequisites in place, the next step is to call presenter’s search() method. The last step is the verification step, also very easy to understand, no nonsense.

We also missed two methods eq(“eggs”) and any(), both of which return Matcher objects that, as the name suggests, verify that the parameters match what is expected. Any () is a special Matcher, meaning we don’t care what it is. Note that if a method is called with a parameter that uses Matcher, all other parameters must also be Matcher. You don’t need to remember this. If you make a mistake, the runtime will report an error.

From the previous example, it’s easy to imagine adding a test case that calls view.showerror () when the search fails and view.showempty () when the search result is empty.

These test cases are written to verify that certain methods of the module that the test object depends on can be called correctly, so they fall into a category called behavioral validation, which is what Mockito is usually used to do.

State validation

Another type of test, called state validation, usually uses the Assert function in the JUnit library, for example. Presenter has a method called addFavorite() to add a recipe as a favorite. Let’s look at how to write a test case.

@Test
fun addFavorite_shouldUpdateRecipeStatus(a) {
    val recipe = Recipe("id"."title"."imageUrl"."sourceUrl".false)
    presenter.addFavorite(recipe)
    assertThat(recipe.isFavorited, `is` (true))}Copy the code

Once again, we construct a recipe with the favorited property default false, call the addFavorite() method, and verify that the recipe object’s isFavorited property should be True. AssertThat () is used in the JUnit library. This method takes two arguments, the first is the target of the validation and the second is a Matcher, which needs to be escaped with ‘because kotlin is a reserved keyword.

Similarly, you can add test cases to the removeFavorite() method of Presenter.

Complete test class

Now that we can write a complete test class for Presenter, take a look at the complete code.

class SearchResultsPresenterTests {

    private lateinit var repository: RecipeRepository
    private lateinit var presenter: SearchResultsPresenter
    private lateinit var view: SearchResultsPresenter.View

    @Before
    fun setup(a) {
        repository = mock()
        view = mock()
        presenter = SearchResultsPresenter(repository)
        presenter.attachView(view)
    }

    @Test
    fun search_callsShowLoading(a) {
        presenter.search("eggs")
        verify(view).showLoading()
    }

    @Test
    fun search_succeed_callShowRecipes(a) {
        val recipe = Recipe("id"."title"."imageUrl"."sourceUrl".false)
        val recipes = listOf(recipe)

        doAnswer {
            val callback: RepositoryCallback<List<Recipe>> = it.getArgument(1)
            callback.onSuccess(recipes)
        }.whenever(repository).getRecipes(eq("eggs"), any())

        presenter.search("eggs")

        verify(repository).getRecipes(eq("eggs"), any())
        verify(view).showRecipes(eq(recipes))
    }

    @Test
    fun search_error_callShowError(a) {
        doAnswer {
            val callback: RepositoryCallback<List<Recipe>> = it.getArgument(1)
            callback.onError()
        }.whenever(repository).getRecipes(eq("eggs"), any())

        presenter.search("eggs")

        verify(repository).getRecipes(eq("eggs"), any())
        verify(view).showError()
    }

    @Test
    fun addFavorite_shouldUpdateRecipeStatus(a) {
        val recipe = Recipe("id"."title"."imageUrl"."sourceUrl".false)
        presenter.addFavorite(recipe)
        assertThat(recipe.isFavorited, `is` (true))}@Test
    fun removeFavorite_shouldUpdateRecipeStatus(a) {
        val recipe = Recipe("id"."title"."imageUrl"."sourceUrl".true)
        presenter.removeFavorite(recipe)
        assertThat(recipe.isFavorited, `is` (false))}}Copy the code

This is a relatively complete test class. On the left side of the first line of the class declaration, there is also a button to run all test cases defined in the entire class. There is also a shortcut key Ctrl + Shift + R to run the cursor over the class. The following figure shows the result.

How do you judge the validity of a test

The test code gets written pretty quickly, so you might be thinking, how do you measure the effectiveness of your test? Another concept is introduced here, called Code Coverage.

Test coverage has different dimensions, such as number of classes, number of methods, number of lines, conditional branches, etc., which are beyond the scope of this article and you can explore for yourself. Android Studio has built-in tools to help us with statistics.

Recall that when we ran the test case, Android Studio created a Task for us, and to the right of the Run button, there was a button called “Run [test-task-name] with coverage.” This is the built-in IDE test coverage tool.

A Coverage result window automatically opens after the test, and you can click on it to see a Coverage of the relevant code being tested by the current test task. The results showed that our test case covered 100% of the classes and methods and 88% of the lines.

Click to open the concrete class, you can also see whether each line of code has been executed. It is very useful and provides a good reference value for us to adjust and improve the test cases. For example, looking at the addFavorite() method, our test case did not override the Refresh method call on the View.

Trap alert!

While it may seem that test coverage is a good indicator of unit test coverage and even test quality, it is true that many developers will strive for 100% test coverage because of this, but is that really a good idea?

“Unit testing is not as much as it can be, but as effective as it can be.” This is not from me, but from Kent Beck, the originator of TDD and XP, and the founder of Agile development. The purpose of testing is to improve the quality of your code. As long as you and your team are confident, you can test as much as you like and as appropriately as you like. There is no need to write tests.

read

OK, at this point, you should have learned the basics of writing Android unit tests. If you want to learn more about Android testing, read the following:

  • If relative Android test systematic understanding, you can refer to Google official documentation developer.android.com | testing fundamentals
  • If you want to understand Android UI Testing, you can refer to codeLab Android Testing codeLab, which Google just updated in IO ’19
  • Check out mockito-Kotlin and Mockito’s wiki to learn more about mocks
  • CoolShell has an article on “How detailed should unit tests be?” | cool shell – CoolShell

Happy unit testing!

reference

  • android unit testing with mockito | raywenderlich.com
  • “How detailed should unit tests be?” | cool shell – CoolShell
  • The Way of Testivus – Unit Testing Wisdom From An Ancient Software Start-up

@monkeyM