Espresso Extensions
Espresso was originally written in Java which can be verbose and makes heavy use of static functions which do not always show up as suggestions when trying to use code completion. Since these tests are written in Kotlin, extension functions can be utilized to help improve the developer experience in writing expressive tests that provide helpful code completion.
ViewInteraction Extension
One of the most common ways to create a ViewInteraction
with a View
id is onView(withId(R.id.some_id))
. To simplify this construction in page objects, an extension function can be added to the BasePage
interface on an Int (resource id) which will return the ViewInteraction
.
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers.withId
interface BasePage {
fun Int.toViewInteraction(): ViewInteraction =
onView(withId(this))
}
Now the LoginPage
can take advantage of the extension function for the implementation detail of the page objects.
class LoginPage : BasePage {
fun onEmail(): ViewInteraction = R.id.username.toViewInteraction()
fun onPassword(): ViewInteraction = R.id.password.toViewInteraction()
fun onSignInOrRegisterButton(): ViewInteraction = R.id.login.toViewInteraction()
}
ViewAction Extensions
There are many of different types of actions that could be taken on a View, this series makes use of the following extension functions on a ViewInteraction
in order to write more expressive tests. If you need a different type of action, the pattern should still apply simply creating an extension function on ViewInteraction
.
Visibility
fun ViewInteraction.verifyVisible(): ViewInteraction = check(matches(isDisplayed()))
The assertScreen()
function in each page can be implemented using the visibility extension function.
class LoginUI : BaseUI {
override fun assertScreen() {
onEmail().verifyVisible()
onPassword().verifyVisible()
onSignInOrRegisterButton().verifyVisible()
}
}
Text Fields
fun ViewInteraction.typeText(text: String): ViewInteraction = perform(ViewActions.typeText(text))
fun ViewInteraction.verifyTextFieldError(@StringRes resId: Int): ViewInteraction =
check(matches(hasErrorText(getString(resId))))
// Helper function for getting strings using application target context
fun getString(@StringRes resId: Int): String =
InstrumentationRegistry.getInstrumentation().targetContext.getString(resId)
One sample usage might be for a test that verifies an error is shown on a password field if not enough characters were entered.
LoginPage().apply {
onPassword().typeText("4344")
onEmail().typeText("andrew@test.com")
onPassword().verifyTextFieldError(R.string.invalid_password)
}
Clicking
fun ViewInteraction.click(): ViewInteraction = perform(ViewActions.click())
Sample:
LoginPage().onSignInOrRegisterButton().click()
Verify Text
fun ViewInteraction.verifyText(text: String): ViewInteraction = check(matches(withText(text)))
Sample:
LoggedInPage().onWelcomeGreeting().verifyText("Welcome Andrew!")
androidx.test:core-ktx
androidTestImplementation "androidx.test:core-ktx:<version>"
Sample:
val scenario = launchActivity<LoginActivity>()
Simple Test
Now that all of the page objects are implemented, the last login test can be simplified further using the new extension functions.
@Test
fun successfulLogin() {
val scenario = launchActivity<LoginActivity>()
LoginPage().apply {
onEmail().typeText("andrew@test.com")
onPassword().typeText("password123")
onSignInOrRegisterButton().click()
}
LoggedInPage().onWelcomeGreeting().verifyText("Welcome Andrew!")
}
The test is starting to look better, however each page has to be instantiated and there is no link between pages when writing tests. Thankfully Kotlin has features that will be covered in the next section to create a navigation DSL.
Resources
- Source code