Testing Android Applications - Part 1 - Introduction
Image source: stocksnap.io

Testing Android Applications - Part 1 - Introduction

Ok, it works, you can try it.

some minutes later: What? No, that looks different on my device! Wtf is going on?

a lot of minutes later: Ok, I figured it out, this is a bug on [insert manufacturer] devices, there this view element behaves differently.

Depending on the platform you are developing for, such a conversation may sound familiar. One way to tackle such moments is testing and Android applications are no exceptions here. I want to share some of my learnings when it comes to software testing of Android apps. This first part provides an overview of some testing fundamentals in the Android world:

Instrumented vs ‘Local’ Tests

When it comes to Android, the first thing is to distinct between Instrumented and Local tests:

  • ‘Local’ tests are running inside Android Studio and have no dependencies on Android APIs.
  • ‘Instrumented’ tests are running on a real device (or an emulator). They have access to android APIs and for example the Context of the app they’re part of.

Unit vs Integration Tests

A second differentiation documented in the android developer guide is between Unit and Automated UI tests:

Unit Tests

Unit Tests are testing the logic of individual units of code. If you write some function that parses a string into a date: you should write a Unit test for it (or do it the other way round and call it TDD). Unit tests typically are meant to test logic. You isolate a piece of logic (by using a mocking framework like mokito) and then verify it with some tests. Since Android is using Java, JUnit is the prefered way to go.

Unit tests can be either local or instrumented. Local unit tests are the fastest since they are executed in the JVM on your local machine.

When you cannot isolate a piece of logic with mock objects (because they need an Android Context or other Android APIs) you go with Instrumented Unit Tests

Automated UI tests

Automated UI tests are not testing seperate units, instead they really launch your application (or a specific activity), interact with the UI and then perform checks on it.

Espresso vs UI Automator

To interact with some Android UI there are again various possibilities. The two best known are espresso and UI Automator.

Espresso is used to interact with the UI of your app. This is also the limit that distinguishes it from UI Automator: Espresso can only interact with views that are directly part of your app while UI Automator is able to interact with all UI elements (be it other apps or UI from the OS itself). This two libraries are not mutually exclusive, on the contrary: They play very well together and it is advisable to combine them when needed.

Examples

Let’s assume we have a simple hello world app: When clicking a “Say Hello” button in the app, the text “Hello World!” appears in a textview. A simple espresso test could look like this:

    @Test
    public void checkHelloWorld() {
        // Click the 'Say Hello' button.
        onView(withId(R.id.hello_button)).perform(click());
        // Check that the text in the label displays the correct value.
        onView(withId(R.id.hello_label)).check(matches(withText("Hello World!")));
    }

Espresso uses hamcrest matchers to match views and then perform actions or checks.

Using UI-automator the same test could look like this:

    @Test
    public void checkHelloWorld() throws UiObjectNotFoundException {
        UiDevice mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        // Find and click the 'Say Hello' button.
        UiObject helloButton = mDevice.findObject(new UiSelector().resourceId("com.autcoding.testapp:id/hello_button"));
        helloButton.click();

        // Find the text view and check if it contains the right text
        UiObject helloLabel = mDevice.findObject(new UiSelector().resourceId("com.autcoding.testapp:id/hello_text"));
        assertTrue(helloLabel.getText().equals("Hello World!"));
    }

When you compare the two implementations you can see that by using espresso you can make use of the resource ids, as you can identify views like this: withId(R.id.hello_button). Using UI-Automator, you have to either know the id as a string like this: new UiSelector().resourceId("com.autcoding.testapp:id/hello_button") or you could find it by its content like this: UiObject helloButton = mDevice.findObject(new UiSelector().text("Say Hello")). In the first approach you have to be careful to use the correct package name (in my example com.autcoding.testapp). The second approach has even more pitfalls: First you have to make sure you use the right translation if you support multiple languages. Secondly you have to give the exact text that is displayed in the UI. So if your UI element displays a given text as uppercase (like some button styles do) you also have to give the text as uppercase text to find the correct view element.

When to use what?

One example where you might want to add UI Automator code to your test cases is when it comes to the permission dialogs introduced in API level 23 (Android 6.0). Those dialogs are not part of your app but are from the Android OS itself, therefore you can not interact with them using espresso. The second “Testing Android Applications” blogpost will cover how to do this and also how to write custom espresso matchers to even more adapt the tests to your needs.