
Like most Android projects, our UI tests use the Espresso framework to perform interactions on our test app, check an element is on the page, check the text of an element etc. Although our tests worked they were quite hard to read and it would take a bit of time for someone who hadn’t seen the test before to work out what the test was doing and what it was checking for. Let’s have a look at an example test to see what I mean.
Example app
We have a really simple app that consists of 3 elements – an EditText, a TextView and an AppCompactButton. The user can put text in the EditText and then when the button is pressed it updates the TextView with the text that was in EditText (I know what you’re thinking, yes this is the best app ever). For clarity the code is below:
MainActivity.kt
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val enterText = findViewById<EditText>(R.id.enter_text) val textLabel = findViewById<TextView>(R.id.text_label) val button = findViewById<AppCompatButton>(R.id.button) button.setOnClickListener { textLabel.text = enterText.text } } }
Normal Espresso Test
First we’ll write a test using Espresso commands. I’m not going to go into detail here about Espresso as I presume you have a basic knowledge of Espresso. We create a rule for our activity, start the activity, enter some text into the EditText, click the button and then check the TextView has the correct text.
MainActivityTest.kt
@RunWith(AndroidJUnit4::class) @LargeTest class MainActivityTest { @get:Rule val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java, true, false) @Test fun should_UpdateText_When_ButtonTapped() { activityRule.launchActivity(Intent()) onView(withId(R.id.enter_text)).perform(typeText("This is some test text."), closeSoftKeyboard()) onView(withId(R.id.button)).perform(click()) onView(withId(R.id.text_label)).check(matches(withText("This is some test text."))) } }
Like I said, it’s not the easiest thing in the world to read, so we decided to take advantage of the Page Object pattern and Kotlin magic.
Page object pattern
The page object is a design pattern that represents the screens of your application as a series of objects and encapsulates the features represented by a page. The page object is an object-oriented class that serves as an interface to a page of the application under test. In our case each activity will have its own page and each page will have the interactions that can be performed e.g. click a button. This will allow us to reuse the same function and make the tests more readable and robust.
First off we’re going to create a new class and call it MainActivityPage. We can then move the activity rule into MainActivityPage and create a function called loadPage. In the method block we put activityRule.launchActivity(Intent()).
MainActivityPage.kt
class MainActivityPage { @get:Rule val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java, true, false) fun loadPage() { activityRule.launchActivity(Intent()) } }
Next we will create a function enterText that takes in a String parameter (called text) and in the method block we put onView(withId(R.id.enter_text)).perform(typeText(text), closeSoftKeyboard()). Then we create a function called clickButton and in the method block put the code that clicks the button. Finally we create a function called textLabel and the method block will contain return onView(withId(R.id.text_label)).
MainActivityPage.kt
fun enterText(text: String) { onView(withId(R.id.enter_text)).perform(typeText(text), closeSoftKeyboard()) } fun clickButton() { onView(withId(R.id.button)).perform(click()) } fun textLabel(): ViewInteraction { return onView(withId(R.id.text_label)) }
Back to our test class, we create a MainActivityPage field and initialise it in a @Before method called setup. Then in the test we call the loadPage function, followed by enterText and then clickButton. We then replace onView(withId(R.id.text_label)) with the textLabel function. Now our test will look like this:
MainActivityTest.kt
@RunWith(AndroidJUnit4::class) @LargeTest class MainActivityTest { private lateinit var page: MainActivityPage @Before fun setUp() { page = MainActivityPage() } @Test fun should_UpdateText_When_ButtonTapped() { page.loadPage() page.enterText("This is some test text.") page.clickButton() page.textLabel().check(matches(withText("This is some test text."))) } }
As you can see our test is getting more readable but there’s still some things we can do better:
- The assertion should be in our test class but is still quite a verbose way of checking the text is correct
- Is there a way we can have a reusable function for clicking any button to limit the amount of Espresso boilerplate?
Espresso Extensions
We’re going to create some Extension functions that we can reuse to make our code more readable and limit the amount of boilerplate we have to write.
First we need a function (which we will call matchView) to do the OnView(…) stuff and return a ViewInteraction which can be used to perform an action against or verify some data for that view.
EspressoExtensions.kt
fun Int.matchView(): ViewInteraction { return onView(withId(this)) }
Next we create a function called type() and put the perform type text code in the method block and then we create a function called clickIt() and put the perform click action in the method block.
EspressoExtensions.kt
fun ViewInteraction.type(text: String): ViewInteraction { return perform(typeText(text), closeSoftKeyboard()) } fun ViewInteraction.clickIt(): ViewInteraction { return perform(click()) }
Now we have extensions for performing actions we need to add some functions for asserting. We create a function called checkThat which will take in a ViewInteraction and return the ViewInteraction (this is so assertions are clearly defined by the function checkThat) and then we create a function called hasText which will take in a String and in the method block we put the check(matches(withText(..))) code.
ExpressoExtensions.kt
fun checkThat(view: ViewInteraction): ViewInteraction { return view } fun ViewInteraction.hasText(text: String): ViewInteraction { return check(matches(withText(text))) }
In MainActivityPage we will replace any reference to onView(withId(…)) with.matchView()
MainActivityPage.kt
fun textLabel(): ViewInteraction { return R.id.text_label.matchView() } fun enterText(text: String) { R.id.enter_text.matchView().perform(typeText(text), closeSoftKeyboard()) } fun clickButton() { R.id.button.matchView().perform(click()) }
The last thing we need to do to the page is replace .perform(typeText(text), closeSoftKeyboard()) with .type(text) and replace .perform(click()) with .click().
MainActivityPage.kt
class MainActivityPage { @get:Rule val activityRule = ActivityTestRule<MainActivity>(MainActivity::class.java, true, false) fun loadPage() { activityRule.launchActivity(Intent()) } fun textLabel(): ViewInteraction { return R.id.text_label.matchView() } fun enterText(text: String) { R.id.enter_text.matchView().type(text) } fun clickButton() { R.id.button.matchView().perform(click()) } }
The last thing we need to do is update the assertion in MainActvityTest. We wrap the page.textLabel() call in the checkThat() function and then replace .check(matches(withText(“This is some test text.”))) with .hasText(“This is some test text.“).
MainActivityTest.kt
@RunWith(AndroidJUnit4::class) @LargeTest class MainActivityTest { private lateinit var page: MainActivityPage @Before fun setUp() { page = MainActivityPage() } @Test fun should_UpdateText_When_ButtonTapped() { page.loadPage() page.enterText("This is some test text.") page.clickButton() page.textLabel().hasText("This is some test text.") } }
Summary
We have used Espresso Extension functions to abstract the verbose Espresso code and the Page Object Pattern to interact with elements on the page. This approach has made our tests easier to understand, allowed us to make changes to tests quicker and help stop the duplication of the same methods by having reusable functions.