Chapters

Hide chapters

Android Test-Driven Development by Tutorials

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2.1

Section II: Testing on a New Project

Section 2: 8 chapters
Show chapters Hide chapters

Section III: TDD on Legacy Projects

Section 3: 8 chapters
Show chapters Hide chapters

11. User Interface
Written by Victoria Gonda

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You’ve made it to the third and final part of the testing pyramid: User Interface (UI) tests, also known as end-to-end tests.

Almost all Android apps have a UI, and subsequently, an essential layer for testing. UI testing generally verifies two things:

  1. That the user sees what you expect them to see.
  2. That the correct events happen when the user interacts with the screen.

With UI tests, you can automate some of the testing you might otherwise need to do with tedious, manual click-testing. A step up from integration tests, these test your app most holistically.

Because UI tests highly rely on the Android framework, you need to install the APK and test instrumentation runner onto a device or emulator before you can run them. Once installed, you can run the tests that use the screen to display and perform actions that verify the behavior. Because of the work involved, UI tests are the slowest and most expensive to run, which means you’re less likely to run them, losing the benefit of quick feedback.

Note: With AndroidX Test, it’s possible to run these tests without a device or emulator and instead run them with Robolectric. This chapter will not elaborate on the specifics as the technique is the same as described in Chapter 8, “Integration.”

Following the TDD process requires running your tests frequently while building, so you won’t want to lean too heavily on UI tests. The length of time it takes to run them will increase the time it takes to write them. Test the logic you need to test with UI tests and push what you can into integration or unit tests. A good rule of thumb is the 10/20/70 split mentioned in Chapter 4, “The Testing Pyramid,” which explains that 10% of your tests should be UI tests. The idea is that you test for the main flows, putting whatever logic you can into classes that you can verify using a faster test.

Introducing Espresso

The main library used for testing the UI on Android is Espresso. Manually click-testing all parts of your app is slow and tedious. With Espresso, you can launch a screen, perform view interactions and verify what is or is not in view. Because this is common practice, Android Studio automatically includes the library for you when generating a new project.

Note: Google’s motivation behind this library is for you “to write concise, beautiful and reliable Android UI tests.”

Getting started

In this chapter, you’ll continue working on the Punchline Joke app that you worked on in Chapter 10, “Testing the Network Layer.” This is an app that shows you a new, random joke each time you press a button.

Getting familiar with the project

In this chapter, you’ll write tests and implementation for MainActivity. Find the following files, so you’re all set to go:

Using Espresso

As is the case when generating a new project in Android Studio, the dependency for Espresso is already included for you.

androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

What makes up Espresso?

There are three main classes you need to know when working with Espresso: ViewMatchers, ViewActions and ViewAssertions:

Setting up the test class

To get started, inside app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ punchline, create a file named MainActivityTest.kt. Add to it, an empty test class using androidx.test.ext.junit.runners.AndroidJUnit4 and org.koin.test.KoinTest for imports:

@RunWith(AndroidJUnit4::class)
class MainActivityTest: KoinTest {
}

Using dependency injection to set mocks

In previous chapters, you used Mockito mocks to stub out some functionality. For example, when you didn’t want to hit the network layer. In many of these cases, you can introduce these mocked classes by passing them through the constructor. But how would you do that for an Activity? You don’t have the same luxury, because the Android framework instantiates the class for you. This is why dependency injection is helpful when it comes to testing.

@get:Rule
val mockProvider = MockProviderRule.create { clazz ->
  Mockito.mock(clazz.java)
}
private val mockRepository: Repository by inject()
private var faker = Faker()

Writing a UI test

This Joke app has a button that makes a new joke appear, so the first test you’ll add checks if this button is visible. Following the usual pattern, this test will have set up, actions and verifications.

@Test
fun onLaunchButtonIsDisplayed() {
  declareMock<Repository> {
    whenever(getJoke())
        .thenReturn(Single.just(Joke(
            faker.idNumber().valid(),
            faker.lorem().sentence())))
  }
}
// 1
ActivityScenario.launch(MainActivity::class.java)
// 2
onView(withId(R.id.buttonNewJoke))
    .check(matches(isDisplayed()))
<Button
    android:id="@+id/buttonNewJoke"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    />
Expected: is displayed on the screen to the user
Got: "AppCompatButton{id=2131165226, res-name=buttonNewJoke...
android:visibility="gone"

Testing for text

When the app is first launched, you expect to see the first joke. In this test, you’ll make sure there’s a view that displays that joke right away.

@Test
fun onLaunchJokeIsDisplayed() {
  // 1
  val joke = Joke(
      faker.idNumber().valid(),
      faker.lorem().sentence())
  declareMock<Repository> {
    whenever(getJoke())
        .thenReturn(Single.just(joke))
  }
  // 2
  ActivityScenario.launch(MainActivity::class.java)
  onView(withId(R.id.textJoke))
      .check(matches(withText(joke.joke)))
}
<TextView
    android:id="@+id/textJoke"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    />
Expected: with text: is "Dolores quia consequatur quos."
Got: "AppCompatTextView{id=2131165357, res-name=textJoke, text=,
binding.textJoke.text = joke.joke

Refactoring

Run the app to see how it’s looking so far. It may not be pretty, but it sure is testable!

style="@style/TextAppearance.MaterialComponents.Headline6"
android:gravity="center_horizontal"
android:padding="16dp"
app:layout_constraintBottom_toTopOf="@+id/buttonNewJoke"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
android:text="@string/new_joke"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textJoke"

Regression testing

It’s relatively easy to keep things from breaking when you’re working with a simple UI like this. But these tests are extremely helpful when you’re working with a complicated UI with nested, reused views. Because you want to limit your UI tests, you may fall into a pattern of introducing regression tests.

Performing an action

There’s one more behavior to test and implement for this app: When a user taps the button, a new joke should appear. This is the final and most complex test you’ll write for this chapter, so you’ll write it in two steps.

@Test
fun onButtonClickNewJokeIsDisplayed() {
  // 1
  val joke = Joke(
      faker.idNumber().valid(),
      faker.lorem().sentence())
  // 2
  val jokeQueueAnswer = object: Answer<Single<Joke>> {
    val jokes = listOf(
        Joke(
            faker.idNumber().valid(),
            faker.lorem().sentence()),
        joke
    )
    var currentJoke = -1
    override fun answer(invocation: InvocationOnMock?): Single<Joke> {
      currentJoke++
      return Single.just(jokes[currentJoke])
    }
  }
  // 3
  declareMock<Repository> {
    whenever(getJoke())
        .thenAnswer(jokeQueueAnswer)
  }
}
ActivityScenario.launch(MainActivity::class.java)
// 1
onView(withId(R.id.buttonNewJoke))
    .perform(click())
// 2
onView(withId(R.id.textJoke))
    .check(matches(withText(joke.joke)))
Expected: with text: is "Error ut sed doloremque qui."
Got: "AppCompatTextView{id=2131165357, res-name=textJoke, ...
     text=Laudantium et quod dolor.,
binding.buttonNewJoke.setOnClickListener {
  viewModel.getJoke()
}

Using sharedTest (optional)

In Chapter 8, “Integration,” you learned that you could run Android tests on either a device or locally using Robolectric. For this to work, your test must be in the correct test/ or androidTest/ directory. With a small configuration change and a new sharedTest/ directory, you’ll be able to run your tests both ways without needing to move the file.

sourceSets {
  String sharedTestDir = 'src/sharedTest/java'
  test {
    java.srcDir sharedTestDir
  }
  androidTest {
    java.srcDir sharedTestDir
  }
}
class JokeTest {

  private val faker = Faker()

  @Test
  fun jokeReturnsJoke() {
    val title = faker.book().title()
    val joke = Joke(faker.code().isbn10(), title)

    assert(title == joke.joke)
  }
}

Running tests from the command line

Using gradle, running your tests from the command line is easy. Open a terminal and navigate to the root directory of your project, or use the terminal view in Android Studio.

./gradlew test
./gradlew connectedAndroidTest

Creating a run configuration

Android Studio also supports creating run configurations, which are presets you create and run that inform Android Studio of how you want things to run.

Key points

  • UI tests allow you to test your app end-to-end without having to manually click-test your app.
  • Using the Espresso library, you’re able to write UI tests.
  • You can run Android tests on a device and locally using Roboelectric.

Where to go from here?

Now that you know the basics of UI testing with Espresso, you can explore and use everything else the library has to offer.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now