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.
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
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
UI Testing with Kakao Tutorial for Android: Getting Started
20 mins
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:
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’.
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.
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:
-
typeText()
to put some text in the input, a ViewAction. -
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:
- Types a query into the ingredients
EditText
and clicks the Search button. - Instantiates a
KIntent
that will verify it’s an instance of theSearchResultsActivity
and has the corresponding EXTRA. - 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:
- Configures an Intent coming from
RecentIngredientsActivity
with a result containing theString
s eggs and onion. - Instructs Kakao to stub the intent you configured above.
- Performs a click on the Recent button so that the stubbed intent is launched.
- 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:
- Scroll to each item.
- Verify it has the corresponding title.
- 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.