30 August 2020

TDD a UI with Fast Tests

AndroidsThis is another part of my series of TDD-ing user interfaces. Earlier this year I looked at test driving classic, fat, state based UIs like Swing or WinForms and then web component libraries like Vaadin. In both situations it was possible to TDD the UI using the Model View Presenter (MVP) pattern and a decent test bed for testing the implementation of the view. Later I explored test driving an immediate mode GUI, which was even easier than doing so for retained mode: There was no need to search for components, capture events or trigger events. This time I want to experiment with test driving a user interface by only using UI tests. In theory I could do that with Swing or Vaadin, but the UI tests of these technologies are too slow. For my TDD cycle I want fast tests!

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.

No comments: