Bloco

View Original

How to test Android Apps: Instrumentation Testing

Although you should avoid having your code tied to Android components, it's not always possible. Maybe it's Activity code, or a component that needs Context. But you still should test it.

Those tests will have to run on an emulator or a real device. They are going to be slower. But they are important nevertheless.

Here's how you can do that:

Testing with application context

Sometimes you just need Context to test your code, but not a full activity. For example, testing a database integration, or a file resource loader. For those cases, you can use the InstrumentationRegistry.

An example:

@RunWith(AndroidJUnit4::class)
class DatabaseTest {

    private val database: Database = 
        Database(InstrumentationRegistry.getInstrumentation().targetContext)

    @Test
    fun testName() {
        Assert.assertEquals(database.getName(), "test.db")
    }
}

In this example, we're testing a Database class that needs a Context.

After creating the application, we can use the targetContext to get the application Context and pass it on as an argument. Careful: there's also a context, however that one does not get the application context, but the instrumentation package context, which you usually don't need.

The rest of the code was just a general test example.

Testing activities

If you want to test an Activity, or a specific view, you need to launch it. These tests are even slower than Application tests. But it's important to have these instrumentation tests, that run like a user interacting with your app.

Activity tests are tedious to write, and often run into race conditions. To ease the load, you should use a UI testing library to help you. The most popular right now is Espresso, and it comes with every new Project on Android Studio. It helps you find views, perform actions, wait for the consequences and check state.

Here's an example of an Activity instrumentation test.

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    @Test
    fun helloWorld() {
        Espresso.onView(withId(R.id.textView))
            .check(
                ViewAssertions.matches(
                    ViewMatchers.withText("Hello World!")
                )
            )
    }
}

The example tests the base Main Activity generated with every new project (we did add an ID to the TextView).

Espresso can find views by id, text, content description, hint, view hierarchy and other ViewMatchers. It can perform actions like clicking, swiping, general navigation and other ViewActions. Finally, Espresso can assert state using the same ViewMatchers, or other ViewAssertions.

Waiting

Espresso already does a good job knowing when to wait before assertions. But if you have custom animations, or another special conditions to wait for, you will need to add them.

Espresso comes with IdlingResource for that, but I've found it a bit too complex for most cases. I ended up implementing a simple Wait helper. Here's how you can use it:

Wait(object : Wait.Condition {
      override fun check(): Boolean {
          return someView.isEnabled()
      }
}).waitForIt()

Conclusion

Instrumentation Tests are more time consuming than JVM unit tests but allow you to use Android Resources, and sometimes there's no escaping that.

On the next and final article, called "Other details", we'll discuss things like mocking components and servers, generating fake data and tracking code test coverage.

How to test Android Apps

  1. Introduction
  2. Architecture
  3. Unit Testing
  4. Instrumentation Testing
  5. Other details