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)
}
- The first argument is the class of the activity.
- The second one regards initial touch mode (it is
false
by default, and usually, I don’t need to change it). - And the last argument tells whether the activity should be launched automatically before each test method
(by default it is set to
true
).
There are several problems with this snippet of code:
- 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. - 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)
- 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 ActivityTestRule
s.
Full implementation with examples is available here.
Bibliography
- Functions, Kotlin Programming Language Reference
- Inline Functions, Kotlin Programming Language Reference
- Espresso Documentation
- Testing UI for a Single App, Android Developers