Android UI Tests
My friend Bastien David told me that Android UI tests are pretty fast and I talked him into running this experiment with me. I had never done any Android development and had only little knowledge of the Kotlin language - it is good to have friends who know. Bastien's Kotlin/Android starting point used Espresso and Robolectric for testing the Android UIs. The sample test
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.activityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class HelloActivityTest { @get:Rule var activityScenarioRule = activityScenarioRule<HelloActivity>() @Test fun hello_activity_has_some_hello_text() { onView(withId(R.id.text_hello)).check(matches(withText("Hello!"))) } }used Espresso's matchers to find the text "Hello" on the view of the
HelloActivity
.
First Few Tests
We worked on the same exercise used in my previous articles, the Login Form exercise. The first tests for UI elements of the
LoginActivity
, i.e. user name field and login button were@RunWith(AndroidJUnit4::class) class LoginActivityTest { @get:Rule var activityScenarioRule = activityScenarioRule<LoginActivity>() @Test fun `has username field with max length of 20`() { onView(withId(R.id.username)) .check(matches(checkMaxLength(20))) // custom matcher } @Test fun `has label for username`() { onView(withId(R.id.username_label)) .check(matches(withText("Phone, email or username"))) } @Test fun `has login button`() { onView(withId(R.id.login_button)) .check(matches(withText("Log in"))) .check(matches(not(isEnabled()))) } }These tests did not create any code, we declared the UI elements in the layout
app/src/main/res/layout/activity_login.xml
to make each test pass. On Bastien's machine the tests were fast enough and it was easy to drive UI elements and their attributes. We had to create some custom Hamcrest matchers though, e.g. checkMaxLength
. We decided not to go deeper and assert colours, styles or positions, but we could have.
Test Driving Logic
The next test
@Test fun `when username is introduced then login button is enabled`() { onView(withId(R.id.username)) .perform(typeText("a real username")) onView(withId(R.id.login_button)) .check(matches(isEnabled())) }brought a bit of logic into the
LoginActivity
,class LoginActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) username.addTextChangedListener { login_button.isEnabled = true // new logic } } }We only spent two hours on the exercise and did not get far. Still I could see where we would end. We would inspect every behaviour by its effect on the UI. While we could use the MVP pattern, we did not as the tests were fast enough.
Similar Technologies, e.g. React
Some modern UI technologies come with a fair amount of testing support. For example the same approach should be possible with React. There is a JavaScript/React starting point in the Login Form Kata. Some people have tried that. I do not know if they went far enough for a definite conclusion.
David Tanzer dedicated some of his React TDD videos on testing the UI: In Part 1: Testing the UI Itself he explains the necessary setup to test React and in Part 2: Value and Cost of Tests he talks about possible test cases. He does not check text elements and style of the UI as he considers these things to have little probability of breaking later. Doing TDD, he looks for tests which influence the design of the code. I highly recommend you watch all of his videos.
Conclusion: Is this still TDD?
Test driving a UI with fast tests like Android or React is certainly possible, maybe even easy. These tests are not unit tests. Is it still TDD? We definitely write the tests first so it is Acceptance test driven (A-TDD) or at least "UI specification test driven". Unlike TDD these tests do not influence the design of the code because the components of the UI are usually specified by the requirements, e.g. in wireframes. As all tests exercise the code through the UI, there is no pressure on the code, its interfaces and collaborators. Still we can evolve the code because we have full test coverage and regression safety.