Sławomir Czerwiński

Introduction

In this article, I am using JUnit 4.12, and Espresso 2.2.2 with Espresso Intents.

Since Espresso uses old version of Android Support Annotations (i.e. 23.1.1), it is mandatory to exclude this module when using latest Android Support library:

dependencies {
    // […]
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    androidTestCompile('com.android.support.test.espresso:espresso-intents:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
}

The Problems

When testing Android UI with Espresso, I need to define an ActivityTestRule or an IntentsTestRule (in the examples, I will use the latter). Often, I wish to start the Activity under test with a specific Intent, so the rule must be instantiated by calling a constructor with three arguments:

@RunWith(AndroidJUnit4::class)
class MyTestClass {

    @Rule
    @JvmField
    val mainActivityRule = IntentsTestRule(
            MainActivity::class.java, false, false)
}

There are several problems with this snippet of code:

  1. When I see the constructor call, I don’t know the meaning of each false argument without looking at the definition of the constructor. In Kotlin, I could use named arguments, e.g. launchActivity = false, but it is not allowed with non-Kotlin functions.
  2. I have to provide initialTouchMode, even though I don’t need to change its default value. The second argument is only there to prevent ambiguity, because a constructor:
    // Java code:
    public ActivityTestRule(Class<T> activityClass, boolean launchActivity)
    

    would have the same arguments as the already existing one:

    // Java code:
    public ActivityTestRule(Class<T> activityClass, boolean initialTouchMode)
    
  3. The ::class.java after the activity class makes the code obscure.

All these problems could be easily avoided in Kotlin, but the Espresso library is written in Java.

Using Kotlin Functions

To solve the first and the second issue, I created a simple Kotlin function with default argument values:

fun <T : Activity> intentsTestRule(
        activityClass: Class<T>,
        initialTouchMode: Boolean = false,
        launchActivity: Boolean = true) =
    IntentsTestRule(activityClass, initialTouchMode, launchActivity)

As a result, I can name the third argument and omit the second one:

@RunWith(AndroidJUnit4::class)
class MyTestClass {

    @Rule
    @JvmField
    val mainActivityRule = intentsTestRule(
            MainActivity::class.java,
            launchActivity = false)
}

Using Reified Type Parameters

To solve the last problem, I needed to harness Kotlin reified type parameters.

After reifying parameter T, I can directly access its type inside the function (note that I can only do that with inline functions). Thus, the first argument is no longer needed:

inline fun <reified T : Activity> intentsTestRule(
        initialTouchMode: Boolean = false,
        launchActivity: Boolean = true) =
    IntentsTestRule(T::class.java, initialTouchMode, launchActivity)

So instead of providing the argument, I can just define the type parameter:

@RunWith(AndroidJUnit4::class)
class MyTestClass {

    @Rule
    @JvmField
    val mainActivityRule = intentsTestRule<MainActivity>(launchActivity = false)
}

A similar function might be defined for ActivityTestRules.


Full implementation with examples is available here.

Bibliography