Implementing the Robot Pattern using Espresso in UI testing for Android. (Part 2)

Bego Ros
5 min readDec 18, 2020

--

Step by step guide to implement UI test using the Robot Pattern.

What is Espresso?

Espresso is the default User Interface testing framework that is used for Android applications as it is part of the Android Framework. It uses the native language of the codebase which, in this case, is Kotlin. Espresso makes writing UI tests easier and it helps to do it in a scalable way. It is clearly documented and there are plenty of resources both on tutorial sites and the official Android Developer Documentation.

Implementation steps

Before we start, I recommend you read part 1 of this article as it covers in detail the architecture and pattern that we will be following. We will start writing some UI tests for the Splash, Login and Registration screens of my app ‘Guinea Gram’ which I am currently building. These are the flows we we will be automating:

  1. Let’s start by creating our BaseRobot class to hold our action functions so they are reusable for all our other tests. The rest of your specific robot classes for each screen will extend from BaseRobot. Here is the code with the functions we will need to run our tests.

BaseRobot class →

abstract class BaseRobot {

/*
Handles clicking of buttons with identifiers for resources Id and String
*/
fun clickButton(resId:Int): ViewInteraction =
onView(withId(resId)).perform(
click()
)

/*
Adds text to EditTextView using resource Id and String to match with
*/
fun fillEditText(resId: Int, text: String): ViewInteraction =
onView(withId(resId)).perform(
typeText(text),
closeSoftKeyboard()
)
/*
Checks if views are shown using resource id
*/
fun isViewShown(resId: Int): ViewInteraction =
onView(withId(resId)).check(matches(isDisplayed()))


/*
Checks if the error hints are shown on TextInputs
*/
fun checkErrorOnTextInputLayout(resId: Int, error: String) =
onView(withId(resId)).check(matches(textInputLayoutErrorShown(error)))
}

2. Now we need to create our Baste Test class. All of out test classes will extend from BaseTest. Here we initialise the application and pass the fragments that we want to launch for the tests we are intending to run.

BaseTest class →

//Preparing set up to run the tests, tests extend from this class

typealias FragmentLaunchMethod = () -> Fragment

@RunWith(AndroidJUnit4::class)
abstract class BaseTest(private val fragment: FragmentLaunchMethod) {

@Before
open fun setUp() {
InstrumentationRegistry.getInstrumentation().targetContext.apply {
this.theme.applyStyle(R.style.Theme_GuineaGram, true)
}
launchFragmentInContainer(themeResId = R.style.Theme_GuineaGram, instantiate = fragment)
}
}

3. Now we are going to create specific Robots for the Splash, the Login and the Registration Screens. These robots will include a nested class called AssertRobot. The main responsibility of this nested inner class is to make assertions on the screen. This allows for a clean distinction between the actions and the assertions.

Following the Single Responsibility Principle, we will have a separate Robot Class for each different screen. This means, that if we want to verify if the user has landed on the Registration and Login Screens after having tapped on the corresponding buttons, then we need to create the Robots for these two screens too as per below:

SplashRobot class

fun splashScreen(func: SplashScreenRobot.() -> Unit) = SplashScreenRobot().apply { func() }

class SplashScreenRobot : BaseRobot() {

fun tapOnLoginButton() {
clickButton(R.id.buttonLogin)
}

fun tapOnNewToGuineaGram() {
clickButton(R.id.buttonNewToGG)
}

infix fun assert(func: AssertRobot.() -> Unit) = AssertRobot().apply { func() }

class AssertRobot: BaseRobot() {

fun isImageOfGuineaPigWavingDisplayed() {
isViewShown(R.id.guineaPig)
}
}
}

LoginRobot class

fun loginScreen(func: LoginScreenRobot.() -> Unit) = LoginScreenRobot().apply { func() }

class LoginScreenRobot : BaseRobot() {

fun enterEmailAddress(email: String) {
fillEditText(R.id.email, email)
}

fun enterPassword(password: String) {
fillEditText(R.id.password, password)
}

fun tapOnLoginButton() {
clickButton(R.id.buttonLogin)
}

infix fun assert(func: AssertRobot.() -> Unit) = AssertRobot().apply { func() }

class AssertRobot: BaseRobot() {

fun isLoginScreenShown() {
isViewShown(R.id.forgotPassword)

}
}
}

RegistrationScreenRobot class

fun registrationScreen(func: RegistrationScreenRobot.() -> Unit) = RegistrationScreenRobot().apply { func() }

class RegistrationScreenRobot : BaseRobot() {

fun enterName(name: String) {
fillEditText(R.id.firstName, name)
}

fun enterSurname(lastName: String) {
fillEditText(R.id.lastName, lastName)
}

fun enterEmail(email: String) {
fillEditText(R.id.email, email)
}

fun enterPassword(password: String) {
fillEditText(R.id.password, password)
}

fun tapOnRegisterButton() {
clickButton(R.id.register)
}

infix fun assert(func: AssertRobot.() -> Unit) = AssertRobot().apply { func() }

class AssertRobot: BaseRobot() {

fun isRegistrationScreenShown() {
isViewShown(R.id.register)
}

fun isErrorShownForFirstName() {
checkErrorOnTextInputLayout(R.id.firstNameWrapper, "Form cannot be empty")
}

fun isErrorShownForSurname() {
checkErrorOnTextInputLayout(R.id.lastNameWrapper, "Form cannot be empty")
}

fun isErrorShownForPassword() {
checkErrorOnTextInputLayout(R.id.passwordWrapper, "Password should contain 8 characters")
}

fun isErrorShownForEmail() {
checkErrorOnTextInputLayout(R.id.emailWrapper, "Invalid Email")
}
}

4. Now that we have our BaseRobot and our Splash, Login and Registration Robots, we are ready we can start coding our tests! Let’s create a class called SplashScreenTests. This class extends from the BaseFragment as I am launching the fragments individually. Use the functions in your robots to construct your tests! I have also added the Gherkin syntax for every test to make the scenarios clearer to the reader but this is personal preference.

SplashScreenTests class →

class SplashScreenTest : BaseTest(::SplashFragment) {

/*
Given: the user is on the Splash screen
When: the user taps on the Login button
Then: they are presented with the login screen
*/


@Test
fun userTapsOnLoginButtonToNavigateToLoginScreen() {
splashScreen
{
tapOnLoginButton()
}
loginScreen {} assert {
isLoginScreenShown()
}
}

/*
Given: the user is on the Splash screen
When: the user taps on New To Guinea Gram button
Then: they are presented with the Registration screen
*/

@Test
fun userTapsOnNewToGuineaPigButtonToNavigateToRegistration() {
splashScreen
{
tapOnNewToGuineaGram()
}
registrationScreen {} assert {
isRegistrationScreenShown()
}
}
}

LoginScreenTests class →

class LoginScreenTest: BaseTest(::LoginFragment) {

/**
* Given: the user is on the Login Screen
* When: they enter a valid email & password
* Then: they can tap on the login button
*/
@Test
fun userEntersEmailAddressAndPassword() {
loginScreen
{
enterEmailAddress("bubu@bubu.com")
enterPassword("CutiePie123")
tapOnLoginButton()
}
}
}

RegistrationScreenTest class →

class RegistrationScreenTest: BaseTest(::RegistrationFragment) {

/**
* Given: the user is on the Registration screen
* When: they enter a valid email, password, name and surname
* Then: the validation passes
*/

@Test
fun completeRegistrationFormWithValidValues() {
registrationScreen
{
enterName("Dara")
enterSurname("Ross")
enterEmail("bubu@bubu.com")
enterPassword("GPigsAreCool")
tapOnRegisterButton()
assert {
isHomeScreenShown()
}
}
}

/**
* Given: the user is on the Registration screen
* When: they enter an invalid email, password, name and surname
* Then: the validation fails
*/

@Test
fun completeRegistrationFormWithInvalidValues() {
registrationScreen
{
enterName("")
enterSurname("")
enterEmail("nnn")
enterPassword("ccc")
assert {
isErrorShownForFirstName()
isErrorShownForSurname()
isErrorShownForPassword()
isErrorShownForEmail()
}
}
}
}

Tips for your tests:

  • Try to give descriptive names to your functions even if this means they are longer than usual.
  • Keep the robot and test classes clean and ensure you are following the Single Responsibility Principle.
  • Try to keep your tests small and to distinguish between actions on screen and assertions.

Please do let me know if you have any questions or suggestions below :)

--

--