UI Testing with Kakao Tutorial for Android: Getting Started

In this UI Testing with Kakao tutorial for Android, you’ll learn how to create simple, readable UI tests using Kakao and why these are important. By Fernando Sproviero.

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

Running the Test

There are multiple ways to run a test. The simplest one is to press the Play button in the gutter next to the test:

Run test with Play button

You can also place the cursor over the test method and use the shortcut: ^ + ⇧ + R or Control + Shift + R on Windows.

Or you can right click over the SearchUITests.kt file in the Project view and select Run ‘SearchUITests’.
Run test with menu

In any case, you should see a popup to run the test in a device or emulator. Choose one and you’ll see it launches SearchActivity, performs a click on the Search button and shows a Snackbar. If you blink you might miss it!

You’ll see that the test passes.
Passing UI Test

happy face

Adding Other Tests

In the next test you’ll type some text, click the Search button and verify the Snackbar does not show. Because you now need a reference to the ingredients view, add this to the SearchScreen class:

val ingredients = KEditText { withId(R.id.ingredients) }

Now add the following test, underneath the previous one:

  @Test
  fun search_withText_shouldNotShowSnackbarError() {
    screen {
      ingredients.typeText("eggs, ham, cheese")
      searchButton.click()
      snackbar.doesNotExist()
    }
  }

This looks pretty similar to the previous test with just a couple small new things. With how readable Kakao is, you probably already know what’s going on! The new parts you’re using are:

  1. typeText() to put some text in the input, a ViewAction.
  2. doesNotExist() to make sure the Snackbar doesn’t show, a ViewAssertion.

Run the test and check that it passes.

Verifying Intent Launch

The next test you write will verify that the results screen opens after a successful search. To do this, you’ll test for the Intent

To work with intents you need to add the following dependency to app ‣ build.gradle:

  androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.1'

This adds the Espresso library Google provides for working with Intents. Sync your Gradle file so you can use it.

When doing intent testing you need to use IntentsTestRule, so replace the line where you instantiated the ActivityTestRule with the following:

@Rule
@JvmField
var rule = IntentsTestRule(SearchActivity::class.java)

Now, add the following test to the SearchUITests class:

  @Test
  fun search_withText_shouldLaunchSearchResults() {
    screen {
      val query = "eggs, ham, cheese"
      // 1
      ingredients.typeText(query)
      searchButton.click()

      // 2
      val searchResultsIntent = KIntent {
        hasComponent(SearchResultsActivity::class.java.name)
        hasExtra("EXTRA_QUERY", query)
      }
      // 3
      searchResultsIntent.intended()
    }
  }
    Here’s what your new test does:
  1. Types a query into the ingredients EditText and clicks the Search button.
  2. Instantiates a KIntent that will verify it’s an instance of the SearchResultsActivity and has the corresponding EXTRA.
  3. Finally, verifies the intent launches.

Now run the test and see that it passes.

Stubbing the Intent Result

If you have a look at the code of SearchActivity you’ll realize that when you click on the Recent button, it’ll start RecentIngredientsActivity for the result. This last activity allows you to select recent ingredients and returns them back as a list. You can stub the activity result to test this feature!

Just as before you need to add a reference to the new UI element you’re testing. Add the following to the SearchScreen class:

val recentButton = KView { withId(R.id.recentButton) }

To test this feature, add the following code to SearchUITests:

  @Test
  fun choosingRecentIngredients_shouldSetCommaSeparatedIngredients() {
    screen {
      // 1
      val recentIngredientsIntent = KIntent {
        hasComponent(RecentIngredientsActivity::class.java.name)
        withResult {
          withCode(RESULT_OK)
          withData(
              Intent().putStringArrayListExtra(
                  RecentIngredientsActivity.EXTRA_INGREDIENTS_SELECTED,
                  ArrayList(listOf("eggs", "onion"))
              )
          )
        }
      }
      // 2
      recentIngredientsIntent.intending()

      // 3
      recentButton.click()

      // 4
      ingredients.hasText("eggs,onion")
    }
  }
    Here’s what the code you added does:
  1. Configures an Intent coming from RecentIngredientsActivity with a result containing the Strings eggs and onion.
  2. Instructs Kakao to stub the intent you configured above.
  3. Performs a click on the Recent button so that the stubbed intent is launched.
  4. Verifies the ingredients from the stubbed intent are set in the EditText.

Run the test, and it should pass!

Creating a Custom Test Runner

Later, you’ll need to replace the existing recipe repository with a fake implementation to avoid hitting the network in your tests. This implementation will always return the same results which will allow you to have a stable test environment to avoid depending on a network connection and an API returning different results.

To accomplish this, you’ll create a custom TestRunner that instantiates a test application. This test application will contain the special repository implementation mentioned.

Create a new class called IngredisearchTestApp under app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ ingredisearch with this content:

class IngredisearchTestApp : IngredisearchApp() {

  override fun getRecipeRepository(): RecipeRepository {
    return object : RecipeRepository {
      override fun addFavorite(item: Recipe) {}
      override fun removeFavorite(item: Recipe) {}
      override fun getFavoriteRecipes() = emptyList<Recipe>()
      override fun saveRecentIngredients(query: String) {}

      override fun getRecipes(query: String, 
                              callback: RepositoryCallback<List<Recipe>>) {
        val list = listOf(
            buildRecipe(1, false),
            buildRecipe(2, true),
            buildRecipe(3, false),
            buildRecipe(4, false),
            buildRecipe(5, false),
            buildRecipe(6, false),
            buildRecipe(7, false),
            buildRecipe(8, false),
            buildRecipe(9, false),
            buildRecipe(10, false)
        )
        callback.onSuccess(list)
      }

      override fun getRecentIngredients() =
          listOf("eggs", "ham", "onion", "tomato")
    }
  }

  private fun buildRecipe(id: Int, isFavorited: Boolean) =
      Recipe(id.toString(), "Title " + id.toString(), "", "", isFavorited)

}

As you can see in getRecipes(), it always returns ten recipes, the second one is favorited. Looking at buildRecipe() you see all the titles have the format Title ID. getRecentIngredients() holds the list of the recent ingredients that will be returned.

Now create a new class called IngredisearchTestRunner under the same package:

class IngredisearchTestRunner : AndroidJUnitRunner() {
  @Throws(
      InstantiationException::class,
      IllegalAccessException::class,
      ClassNotFoundException::class
  )
  override fun newApplication(
      cl: ClassLoader, 
      className: String, 
      context: Context
  ): Application {
    return super.newApplication(
        cl, IngredisearchTestApp::class.java.name, context)
  }
}

When running any instrumented test this runner will instantiate IngredisearchTestApp.

Finally, open app ‣ build.gradle and modify the testInstrumentationRunner to the following:

testInstrumentationRunner "com.raywenderlich.android.ingredisearch.IngredisearchTestRunner"

Don’t forget to sync the changes you made. Now you’re ready to test some screens that use the network.

RecyclerView Testing

Suppose you want to UI test the search results. For this, you need a way to test a list. As a list by nature has duplicate view IDs, you can’t use the same strategy you used before.

Create a new package called searchResults under app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ ingredisearch and create a new class called SearchResultsUITests with the following test rule. You’ll want the androidx.test.platform.app.InstrumentationRegistry import when given the option:

@LargeTest
class SearchResultsUITests {

  @Rule
  @JvmField
  var rule: ActivityTestRule<SearchResultsActivity> =
      object : ActivityTestRule<SearchResultsActivity>
      (SearchResultsActivity::class.java) {

        override fun getActivityIntent(): Intent {
          val targetContext = InstrumentationRegistry
              .getInstrumentation().targetContext
          val result = Intent(targetContext, SearchResultsActivity::class.java)
          result.putExtra("EXTRA_QUERY", "eggs, tomato")
          return result
        }
      }
}

You need this rule because all the tests you’ll write launch SearchResultsActivity which requires EXTRA_QUERY. By overriding getActivityIntent() on the test rule you’re able to provide this extra.

You need to set up a Screen for this test. Also, because you’ll deal with a RecyclerView and its items, you need to set up a KRecyclerItem for that. Add the following above the SearchResultsUITests. Use import android.view.View for the View class:

class Item(parent: Matcher<View>) : KRecyclerItem<Item>(parent) {
  val title = KTextView(parent) { withId(R.id.title) }
  val favButton = KImageView(parent) { withId(R.id.favButton) }
}

class SearchResultsScreen : Screen<SearchResultsScreen>() {
  val recycler: KRecyclerView = KRecyclerView({
    withId(R.id.list)
  }, itemTypeBuilder = {
    itemType(::Item)
  })
}

You’ll write tests that perform actions and verify state on the title and Favorite button of each item of the list.

The first test will check the size of the list. Add it inside the SearchResultsUITests class:

  private val screen = SearchResultsScreen()

  @Test
  fun shouldRenderRecipesFromRepository() {
    screen {
      recycler {
        hasSize(10)
      }
    }
  }

The test repository you added has ten items and you’re checking if the RecyclerView has rendered this correctly with ten items. Run the test and see it pass.

Your next test will test for the contents of each list item. But first, you need to prepare to scroll!

To perform actions, such as scrolling to an item on a RecyclerView, you need to add the following dependency to your app ‣ build.gradle:

androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.1'

Sync your gradle files after this change.

Now add this test:

  @Test
  fun shouldRenderTitleAndFavorite() {
    screen {
      recycler {
        for (i in 0..9) {
          // 1
          scrollTo(i)
          childAt<Item>(i) {
            // 2
            title.hasText("Title " + (i + 1))
            // 3
            if (i != 1) {
              favButton.hasDrawable(R.drawable.ic_favorite_border_24dp)
            } else {
              favButton.hasDrawable(R.drawable.ic_favorite_24dp)
            }
          }
        }
      }
    }
  }

Knowing the items that the test repository returns, you now need to:

  1. Scroll to each item.
  2. Verify it has the corresponding title.
  3. Check it has the correct drawable indicating if it’s favorited or not. Recall that the second item in the test repository is favorited.

Run the test to see that it passes.