© Denys Zelenchuk 2019
Denys ZelenchukAndroid Espresso Revealedhttps://doi.org/10.1007/978-1-4842-4315-2_3

3. Writing Espresso Tests with Kotlin

Denys Zelenchuk1 
(1)
Zürich, Switzerland
 

The Google I/O event in May 2017 announced official Kotlin support. From that moment, Kotlin popularity skyrocketed among Android developers. Keeping in mind the current trends and considering Google’s announcements about shifting the Android toward Kotlin, which is reflected in the Android documentation and the code examples, we can assume that in two to three years, Kotlin will displace Java.

Figure 3-1 shows Java vs. Kotlin usage prediction, which indicates that Kotlin will soon overtake Java in the Android development world.
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig1_HTML.jpg
Figure 3-1

Kotlin vs. Java usage on Android (source: https://realm.io/realm-report/ )

This chapter explains how to migrate existing Espresso Java tests to Kotlin, lists the possible benefits of writing UI tests in Kotlin, and provides an example of creating Espresso DSL with practical examples and tasks.

Migrating Espresso Java Tests to Kotlin

Kotlin works side-by-side with Java on Android, meaning that you can add Kotlin code to your existing projects and can call Java code from Kotlin and vice versa.

The first step is to tell the Android Studio IDE that the project uses Kotlin by adding the kotlin-gradle-plugin dependency to the project build.gradle file, as shown:
dependencies {
    classpath "com.android.tools.build:gradle:3.1.4"
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.61"
    ...
}
After the project is synched, you can start converting Java classes to Kotlin. This can easily be achieved by selecting a Java file or a package, opening the Code menu, and choosing the Convert Java File to Kotlin File option. You can also right-click the file or package and select this option from the pop-up menu (see Figure 3-2).
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig2_HTML.jpg
Figure 3-2

Converting a Java file to Kotlin

Things can look simple for the test classes files, but can be complicated for complex ViewActions or ViewMatchers. When the IDE convertor can’t handle the code complexity, it will require developer interaction. The dialog box in Figure 3-3 alerts the developer to this fact.
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig3_HTML.jpg
Figure 3-3

Code corrections when converting from Java to Kotlin

You can also paste existing Java code into a Kotlin file. In this case, the IDE will identify that the code in the clipboard was copied from a Java file and will suggest converting it to Kotlin code, as shown in Figure 3-4.
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig4_HTML.jpg
Figure 3-4

Converting Java code from the clipboard to Kotlin

You will be asked to add new imports to the Kotlin file if they are not present, as shown in Figure 3-5.
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig5_HTML.jpg
Figure 3-5

Adding new imports to a file after conversion to Kotlin

The conversion cannot handle methods with multiple imports. This requires manual interaction from the developer as well (see Figure 3-6).
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig6_HTML.jpg
Figure 3-6

Multiple choices when converting from Java to Kotlin

The following shows an example of an Espresso UI test method conversion from Java to Kotlin. As you may notice, there is almost no difference except for the function declaration and semicolons at the end of the lines.

Adding a New TO-DO Test in the Java and Kotlin Languages, Respectively.
@Test
public void addsNewToDo() {
    // adding new TO-DO
    onView(withId(R.id.title)).perform(click());
    onView(withId(R.id.add_task_title))
            .perform(typeText(toDoTitle), closeSoftKeyboard());
    onView(withId(R.id.add_task_description))
            .perform(typeText(toDoDescription), closeSoftKeyboard());
    onView(withId(R.id.fab_edit_task_done)).perform(click());
    // verifying new TO-DO with title is shown in the TO-DO list
    onView(withText(toDoTitle)).check(matches(isDisplayed()));
}
@Test
fun addsNewToDo() {
    // adding new TO-DO
    onView(withId(R.id.title)).perform(click())
    onView(withId(R.id.add_task_title))
            .perform(typeText(toDoTitle), closeSoftKeyboard())
    onView(withId(R.id.add_task_description))
            .perform(typeText(toDoDescription), closeSoftKeyboard())
    onView(withId(R.id.fab_edit_task_done)).perform(click())
    // verifying new TO-DO with title is shown in the TO-DO list
    onView(withText(toDoTitle)).check(matches(isDisplayed()))
}

You can see more examples of converting Java files to Kotlin—based on the examples implemented in the ViewActionsTest.kt, RecyclerViewActionsTest.kt, and DataInteractionsTest.kt classes—in the chapter3/testsamples package.

Exercise 10

Converting Java Code to Kotlin
  1. 1.

    Convert an existing Java file to Kotlin.

     
  2. 2.

    Convert a package containing multiple Java files to Kotlin.

     
  3. 3.

    Copy a Java code sample and paste it into a Kotlin file. See what happens if you paste only half of the Java method. Will the conversion be correct?

     

Benefits of Writing Tests in Kotlin

Bringing Kotlin into your test codebase has many advantages. Among them are these:
  • Function as a type support

  • Extension functions

  • String templates

  • Ability to import R.class resources

  • Much cleaner code

Function as a Type

This process saves a function in a variable and then uses it as another function argument or returns a function by another function. In the following example, you can see how the Espresso ViewMatchers.withText() function is returned as a value of the viewWithText() function:
fun viewWithText(text: String): ViewInteraction =
                Espresso.onView(ViewMatchers.withText(text))

Extension Functions

Extensions do not actually modify the classes they extend. By defining an extension, you do not add new members into a class, but only make new functions callable with the dot-notation on instances of this type. With the help of extension functions, the Espresso perform(ViewAction.typeText()) function can be represented in the following way:
fun ViewInteraction.type(text: String): ViewInteraction =
                perform(ViewActions.typeText(text))

In this example, we extended the ViewInteraction class with an additional type() method.

String Templates

Strings may contain template expressions, i.e. pieces of code that are evaluated and whose results are concatenated into the string. A template expression starts with a dollar sign ($) and contains a simple name. Take a look at this example:
fun main(args: Array<String>) {
    val i = 10
    println("i = $i") // prints "i = 10"
}
Or consider an arbitrary expression in curly braces:
 fun main(args: Array<String>) {
    val s = "abc"
    println("$s.length is ${s.length}") // prints "abc.length is 3"
}

Import R.class Resources

Kotlin—together with the Kotlin Android Gradle plugin—simplifies the way that project resources (including string values, IDs, and drawables) can be accessed. In the following listing, based on the addsNewToDo() test implementation from the chapter3/testsamples/ViewActionsKotlinTest.kt file, you can see how Kotlin allows us to import application resources.

chapter3.testsamples.ViewActionsKotlinTest.kt.
... // other imports and package
import com.example.android.architecture.blueprints.todoapp.R.id.*
class ViewActionsKotlinTest : BaseTest() {
    private var toDoTitle = ""
    private var toDoDescription = ""
    @Before
    override fun setUp() {
        super.setUp()
        toDoTitle = TestData.getToDoTitle()
        toDoDescription = TestData.getToDoDescription()
    }
    @Test
    fun addsNewToDo() {
        // adding new TO-DO
        onView(withId(fab_add_task)).perform(click())
        onView(withId(add_task_title))
                .perform(typeText(toDoTitle), closeSoftKeyboard())
        onView(withId(add_task_description))
                .perform(typeText(toDoDescription), closeSoftKeyboard())
        onView(withId(fab_edit_task_done)).perform(click())
        // verifying new TO-DO with title is shown in the TO-DO list
        onView(withText(toDoTitle)).check(matches(isDisplayed()))
    }
}
Instead of the whole R.class, Android Studio IDE allows you to import only one or several resources (see Figure 3-7).
../images/469090_1_En_3_Chapter/469090_1_En_3_Fig7_HTML.jpg
Figure 3-7

Importing R class resources with Kotlin

Espresso Domain-Specific Language in Kotlin

With the help of the Kotlin extension functions and function as a type support, we can drastically reduce the boilerplate of the test code by implementing Espresso domain-specific language (DSL). The goal of our Espresso DSL is to simplify our test codebase, make it more legible and, most importantly, make our tests easy to write and maintain.

First, we must determine which Espresso functions or expressions we use the most in our UI test codebase:
  • View or data interactions represented by the Espresso.onView() and Espresso.onData() methods—The starting point of every line of Espresso test code.

  • Different view actions, like ViewActions.click(), ViewActions.typeText(), ViewActions.swipeDown(), ViewActions.closeSoftKeyboard(), etc.

  • Plenty of view matchers, which are the most used functions inside the test codebase, since they are used not only to locate elements on the page but also in combination with view assertions check view properties: ViewMatchers.withId(), ViewMatchers.withText(), check(matches(ViewMatchers.isDisplayed())), and so on.

  • Aggregated Hamcrest matchers like Matchers.allOf() or Matchers.anyOf().

  • Recycler view actions such as RecyclerViewActions.scrollToHolder() and RecyclerViewActions.actionOnItem() .

Of course, this list can be extended or reduced based on your needs. It is worth it to highlight that the aim of this paragraph is not to standardize the Espresso DSL with Kotlin, but to provide an example of how it can be done, so that you can apply it to your test projects.

The core Espresso.onView() and Espresso.onData() methods are the first functions we going to work with. Seeing that they always take a parameter view matcher or object matcher, we can convert the whole expression into one single Kotlin function, as follows:
fun viewWithText(text: String): ViewInteraction = Espresso.onView(ViewMatchers.withText(text))
Or in case of onData():
fun onAnyData(): DataInteraction = Espresso.onData(CoreMatchers.anything())
You may notice that the returning types are identical to those returned by the onView() and onData() methods—ViewInteraction and DataInteraction, respectively. Another thing is that it is possible to pass a parameter into the extension function that’s used inside the original one. These examples are using Kotlin local functions (i.e., a function inside another function) to simplify the code and can be represented by the following more complex function declarations:
fun viewWithText(text: String): ViewInteraction {
    return  Espresso.onView(ViewMatchers.withText(text))
}
and
fun onAnyData(): DataInteraction {
    return Espresso.onData(CoreMatchers.anything())
}
Now moving to view actions. It is time to use Kotlin extension function support. Here is how the Espresso click action on a view with text looks:
onView(withText("item 1")).perform(ViewActions.click())
You already know that the onView() method returns a ViewInteraction type containing the perform() public method. Now we are going to declare another function that will replace perform(ViewActions.click()). In order to keep the dot notation for the ViewInteraction class, we are going to extend it with our new function, as follows:
fun ViewInteraction.click(): ViewInteraction = perform(ViewActions.click())
This way, we represent the perform(ViewActions.click()) expression by a simple click() function. This example, using the view with text, looks this way now:
viewWithText("item 1").click()

Here we also keep the right return ViewInteraction type. It’s the same one that is returned by the original perform() method.

The same extension function can be added to the DataInteraction class. The only thing we need to do is replace the ViewInteraction extension class with DataInteraction:
fun DataInteraction.click(): ViewInteraction = perform(ViewActions.click())

That is it. Looking good so far.

Moving forward to view matchers and view assertions where the same approach with extension functions is used. Here is an example of an assertion of a view being displayed:
onView(withText("item 1")).check(matches(isDisplayed()))
The check part of the expression can be replaced with this extension function:
fun ViewInteraction.checkDisplayed(): ViewInteraction =
        check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
This, in combination with the viewWithText() extension function example, is transformed into the following simplified expression:
viewWithText("item 1").checkIsDisplayed()
Again, replacing ViewInteraction with the DataInteraction class adds the same extension function to DataInteraction.
fun DataInteraction.checkDisplayed(): ViewInteraction =
        check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Having DSL samples of the Espresso.onView(), ViewActions, and ViewAssertions methods allows us to compare one of the commonly used raw Espresso expressions with one written in DSL (also assuming that we imported all the Espresso static methods):
onView(withText("item 1")).check(matches(isDisplayed())).perform(click())
Here’s the same line written using DSL:
viewWithText("item 1").checkIsDisplayed().click()
We can apply the same approach to an aggregated allOf() Hamcrest matcher:
check(matches(allOf(withText(), isDisplayed())))
This will turn into the allOf() function, as follows:
fun ViewInteraction.allOf(vararg matcher: Matcher<View>): ViewInteraction {
    return check(ViewAssertions.matches(Matchers.allOf(matcher.asIterable())))
}
And the usage will be as follows:
viewWithId(R.id.title).allOf(withText("item 1"), isDisplayed())
Next, we have the recycler view actions. Similar to the previous examples, we can handle recycler view actions. The following example is based on RecyclerViewActions.actionOnItemAtPosition() and looks the following way:
onView(withId(R.id.tasks_list)).perform(actionOnItemAtPosition(10, scrollTo()));
After applying the DSL to this method, we have the following expression:
fun ViewInteraction.actionAtPosition(position: Int, action: ViewAction): ViewInteraction =
        perform(actionOnItemAtPosition<RecyclerView.ViewHolder>(position, action))
So, the final usage is:
viewWithId(R.id.tasks_list)).actionAtPosition(10, scrollTo())

These examples and even more are defined in the chapter3/EspressoDsl.kt file of our sample project for your reference.

Now it is time to apply our domain specific language to our tests and observe how converted Espresso Kotlin tests look compared to those written using DSL. First let’s look at the ViewActions tests samples implemented in ViewActionsKotlinTest.kt.

The checksToDoStateChange() Test Method Implemented in chapter3. testsamples.ViewActionsKotlinTest.kt .
@Test
fun checksToDoStateChange() {
    // adding new TO-DO
    onView(withId(R.id.fab_add_task)).perform(click())
    onView(withId(R.id.add_task_title))
            .perform(typeText(toDoTitle), closeSoftKeyboard())
    onView(withId(R.id.add_task_description))
            .perform(typeText(toDoDescription), closeSoftKeyboard())
    onView(withId(R.id.fab_edit_task_done)).perform(click())
    // marking our TO-DO as completed
    onView(withId(R.id.todo_complete)).perform(click())
    // filtering out the completed TO-DO
    onView(withId(R.id.menu_filter)).perform(click())
    onView(allOf(withId(R.id.title), withText("Active"))).perform(click())
    onView(withId(R.id.todo_title)).check(matches(not(isDisplayed())))
    onView(withId(R.id.menu_filter)).perform(click())
    onView(allOf(withId(R.id.title), withText("Completed"))).perform(click())
    onView(withId(R.id.todo_title))
            .check(matches(allOf(withText(toDoTitle), isDisplayed())))
}

Now we can compare this to the tests from ViewActionsKotlinDslTest.kt.

The checksToDoStateChange() Test Method Implemented in chapter3. testsamples.ViewActionsKotlinDslTest.kt .
// ViewInteractions used in tests
private val addFab = viewWithId(fab_add_task)
private val taskTitleField = viewWithId(add_task_title)
private val taskDescriptionField = viewWithId(add_task_description)
private val editDoneFab = viewWithId(fab_edit_task_done)
private val todoCheckbox = viewWithId(todo_complete)
private val toolbarFilter = viewWithId(menu_filter)
private val todoTitle = viewWithId(todo_title)
private val allFilterOption = onView(allOf(withId(title), withText("All")))
private val activeFilterOption = onView(allOf(withId(title), withText("Active")))
private val completedFilterOption = onView(allOf(withId(title), withText("Completed")))
@Test
fun checksToDoStateChangeDsl() {
    // adding new TO-DO
    addFab.click()
    taskTitleField.type(toDoTitle).closeKeyboard()
    taskDescriptionField.type(toDoDescription).closeKeyboard()
    editDoneFab.click()
    // marking our TO-DO as completed
    todoCheckbox.click()
    // filtering out the completed TO-DO
    toolbarFilter.click()
    activeFilterOption.click()
    todoTitle.checkNotDisplayed()
    toolbarFilter.click()
    completedFilterOption.click()
    todoTitle.checkMatches(allOf(withText(toDoTitle), isDisplayed()))
}

As you may notice, the test method implemented with DSL is much more legible and clean. For even more readability, we declared all used view interactions at the beginning of the test class. This makes the tests even smoother.

Exercise 11

Practicing Espresso DSL Usage
  1. 1.

    Look through the tests implemented in the DataInteractionKotlinDslTest.kt and RecyclerViewActionsKotlinDslTest.kt classes and understand how DSL was applied to these tests.

     
  2. 2.

    Based on the editsToDo() test method from ViewActionsKotlinTest.kt, finish implementation of the editsToDoDsl() test case located in ViewActionsKotlinDslTest.kt using DSL.

     

Summary

After many years of the Java language dominating the Android platform, Kotlin brings a fresh and progressive approach to its applications and to test development. Tests written in Kotlin are more legible, cleaner, and easier to maintain. Its extension functions support allows developers to easily create and test domain-specific language, which simplifies the test code even more. Migration from Java to Kotlin is painless and fast. In the end, it is clear that at some point Kotlin will replace Java in Android application development. You should be prepared to at least migrate to Kotlin and improve your test code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.157.142