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.

5/5 2 Ratings · Leave a Rating

Version

  • Kotlin 1.3, Android 4.1, Android Studio 3

There are several ways to test an app. One of the most important ways is creating UI Tests. These tests run on a device or emulator and interact with the screens of your app, emulate user behavior and verify UI results.

Although UI tests are slow and expensive, they are important because they help to avoid unexpected results or bad user experiences by simulating various possible user scenarios.

In this tutorial you’ll learn how to use Kakao to write UI tests. UI tests on Android are typically written using Espresso. Kakao a library that provides a Domain Specific Language (DSL) for writing expressive Espresso tests.

Note: This tutorial assumes you have previous experience with developing for Android in Kotlin. If you are unfamiliar with the language have a look at this tutorial. If you’re beginning with Android, check out some of our Getting Started and other Android tutorials.

Espresso and Kakao

Espresso is an open source testing framework from Google. They created it to provide an API to create UI tests with the following characteristics in mind:

  • Small
  • Predictable
  • Easy to learn API

Kakao is a library built on top of Espresso. Agoda, which has created more than a thousand automated tests for their codebase, developed it when they realized the code readability of their tests was quite low when using Espresso. They developed this library pursuing the following benefits:

  • Readability
  • Reusability
  • Extensible DSL

Getting Started

Throughout this tutorial you’ll work with IngrediSearch, an app that allows users to search for recipes and favorite them.

To start, download the sample project using the Download Materials button at the top or bottom of this tutorial. Open Android Studio 3.3 or later, click File ▸ New ▸ Import Project and select the top-level project folder for the starter project you downloaded. Alternatively, to open the project you can select Open an existing Android Studio project from the Welcome screen, again choosing the top-level project folder for the starter project you downloaded.

Before you can run the app, you need an API key. This app relies on the Food2Fork API, which requires an API key. To set this up:

  • Get your Food2Fork API key by creating an account here
  • Create a keystore.properties file in the root project. File location screenshot
  • Add the following content to the file, placing the API key you just got within the quotes: FOOD2FORK_API_KEY="YOUR API KEY"

You’re all set. Build and run the project to become familiar with it:

app welcome page app search page
app results page app recipe detail page

The project contains the following main files:

  • MainActivity.kt contains the main screen.
  • SearchActivity.kt allows the user to input ingredients.
  • SearchResultsActivity.kt searches for recipes using the API and shows the results. It also provides the ability to add or remove favorites.
  • RecipeActivity.kt shows the recipe detail.
  • FavoritesActivity.kt shows the list of favorites.
  • RecipeRepository.kt interacts with the API to search for recipes. It also stores the favorites in SharedPreferences.
  • RecipeAdapter.kt is an adapter used to show the list in SearchResultsActivity and FavoritesActivity.

Note: Because this project was also used for a Unit Test Tutorial, you’ll find other files under the app ‣ src ‣ test ‣ java ‣ com ‣ raywenderlich ‣ android ‣ ingredisearch directory. These files contain unit tests which are different from UI tests.

Unit tests are smaller tests that verify class method outputs or state based on a set of given input. Check the Unit Test Tutorial to know more about them.

Setup

You need to make sure you have all your dependencies and configurations ready to start testing. Open build.gradle for the app module and add the following dependencies:

  androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
  androidTestImplementation 'androidx.test:runner:1.1.1'
  androidTestImplementation 'androidx.test:rules:1.1.1'
  androidTestImplementation 'com.agoda.kakao:kakao:2.0.0'

The first three dependencies are part of Android’s standard testing libraries. The fourth is the Kakao library.

Also add the testInstrumentationRunner line below to the same file. The android and defaultConfig lines are to help you know where in the file to place the line:

android {
  ...
  defaultConfig {
    ...
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

AndroidJUnitRunner is the entry point for running the entire suite of instrumentation tests. It controls the test environment and test APK and launches all of the tests defined in your androidTest package.

Sync your Gradle files after these changes.

Note: When you create a new project, Android Studio will include the test instrumentation runner and some of the dependencies mentioned. Here you are adding them manually for educational purposes.

Creating UI Tests

Basics

UI tests involve the following steps:

  1. Finding the view: First, you’ll look for a view where you’ll perform actions or verify states. You’ll use ViewMatchers for this. Normally, you’ll find a view by its id.
  2. Performing an action: After finding the view, you’ll perform some kind of action on it such as a click or a gesture. These are ViewActions. This step is optional and you only need it if there’s an action you want to perform.
  3. Verifying: Finally, you’ll check something on the view. For example, you might check if it’s visible, has changed the position or if the text or color changed. These are ViewAssertions.

Creating Your First UI Test

Create a package called search under app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ ingredisearch. Notice that you’re putting it under androidTest instead of test. This is because tests that require a device or emulator are instrumented tests, which go in this directory.

Your first test will check that searching with an empty text shows a Snackbar with an error message.

First, the test set up. Create a new class called SearchUITests in the directory you just created. Then, place the following class in the same file above the SearchUITests class:

class SearchScreen : Screen<SearchScreen>() {
  val searchButton = KButton { withId(R.id.searchButton) }
  val snackbar = KView { 
    withId(com.google.android.material.R.id.snackbar_text) 
  }
}

You want to import com.raywenderlich.android.ingredisearch.R when prompted, and the com.agoda.kakao.* imports for the others. Unless otherwise noted, default to these imports for this tutorial. You can add imports automatically using Alt + Enter with your cursor placed on the unresolved reference.

Kakao requires a class that inherits from Screen. In this Screen you add the views involved in the interactions of the tests. This can represent a portion or the whole user interface.

The above code sets up the Screen for the search screen. In in you have references to the search button and the Snackbar views. You find them both with a ViewMatcher. In this case, you find the button and the Snackbar with their ids.

With that set up, it’s time to write the test. Replace the SearchUITests class with the following:

// 1
@LargeTest
class SearchUITests {

  // 2
  @Rule
  @JvmField
  var rule = ActivityTestRule(SearchActivity::class.java)

  // 3
  private val screen = SearchScreen()

  // 4
  @Test
  fun search_withEmptyText_shouldShowSnackbarError() {
    // 5
    screen {
      searchButton.click()
      snackbar.isDisplayed()
    }
  }
}
    Here’s what that code does:
  1. Annotates the class with @LargeTest. This is not required but is recommended because you can run a group of tests with this annotation. In the Android ecosystem functional UI tests are considered large tests.
  2. Annotates with @Rule and instantiates an ActivityTestRule. This launches the activity before running each test.
  3. Instantiates the screen where you’ll perform actions and verifications.
  4. Annotates the method with @Test to let the test runner know that this method is a test.
  5. Using the screen object, performs a click, the ViewAction, on the button and verifies that the Snackbar displays, the ViewAssertion.

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.

Where To Go From Here?

Congratulations! You now know the basics of UI Testing using Kakao.

You can download the final project using the Download Materials button at the top or bottom of the tutorial.

As a challenge, you can add the following tests:

  • Scroll to the first item, which is not favorited, click it and verify it changes to the correct drawable.
  • Scroll to the second item, which is favorited, click it and verify it changes to the correct drawable.
  • Click on a recipe and check that the correct url is passed to the RecipeActivity. For this one you’ll need to make sure one of the recipes in the IngredisearchTestApp have a source URL.

The solution can be found in the materials for this chapter.

Here are other references related to the subject:

  • Espresso Cheatsheet: Check this cheatsheet to find ViewMatchers, ViewActions and ViewAssertions.
  • Unit Testing Tutorial: Check this tutorial to see other kinds of tests, where instead of UI you verify state and behavior of isolated classes.
  • TDD Tutorial: This tutorial will introduce you to Test Driven Development.
  • Google Testing Fundamentals: Check the Google reference here.
  • Espresso Guide: To know more about Espresso, check this reference.
  • You can also check this tutorial to know more about UI Tests using Espresso and Screen Robots to avoid code duplication.

I hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

2 ratings

Contributors

Comments