Android provide instrumented tests, which are test which are run on a final device or emulator. This helps to test functionalities in a real environment. Also it enables to create automated tests for the GUI.

A helpful document is the espresso cheat sheet, which I recommend you to checkout.

Add dependencies

Creating a new android project, all needed dependencies should be already included. Also, there should be file ExampleInstrumentedTest.java. Run this file to test, if Instrumented tests work

Recomendation

Add mockito, hamcrest and awaitability to the test dependencies.

  • hamcrest has great comparison functions

  • mockito enables simple mocking

  • awaitility helps with asynchrone function tests, preventing the need for Thread.sleep() calls.

Following includes enables all of them:

app/build.gradle
ext {
    // ...
    // test libraries
    mockitoVersion = "3.1.0"
    hamcrestVersion = "2.1"
    awaitilityVersion = "4.0.1"
}
dependencies {
    // ...
    testImplementation('junit:junit:4.12') {
        exclude group: 'org.hamcrest' (1)
    }
    testImplementation('com.android.support.test:rules:1.0.2') {
        exclude group: 'org.hamcrest' (1)
    }
    testImplementation('com.android.support.test:runner:1.0.2') {
        exclude group: 'org.hamcrest' (1)
    }
    testImplementation "org.awaitility:awaitility:${project.awaitilityVersion}"
    testImplementation "org.hamcrest:hamcrest:${project.hamcrestVersion}"
    testImplementation "org.mockito:mockito-core:${project.mockitoVersion}"

    androidTestImplementation('androidx.test.ext:junit:1.1.0') {
        exclude group: 'org.hamcrest'
    }
    androidTestImplementation('androidx.test.espresso:espresso-core:3.1.1') {
        exclude group: 'org.hamcrest'
    }
    androidTestImplementation('com.android.support.test:rules:1.0.2') { (2)
        exclude group: 'org.hamcrest'
    }
    androidTestImplementation('com.android.support.test:runner:1.0.2') {
        exclude group: 'org.hamcrest'
    }
    androidTestImplementation "org.awaitility:awaitility:${project.awaitilityVersion}"
    androidTestImplementation "org.hamcrest:hamcrest:${project.hamcrestVersion}"
    androidTestImplementation "org.mockito:mockito-core:${project.mockitoVersion}"
}
1 needed, since awaitility is dependent on a newer hamcrest version. This will prevent build errors
2 if missing, strange permission errors will popup

Simple activity start

To get a feeling for instrumented tests, lets just start an activity.

app/src/androidTest/../StartMainActivityInstrumentedTest.java
package ch.amk.exercise4.mqtt;

import android.Manifest;

import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.GrantPermissionRule;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class StartMainActivityInstrumentedTest {

    @Rule
    public ActivityScenarioRule<MainActivity> rule = new ActivityScenarioRule<>(MainActivity.class); (1)

    @Test
    public void testStartingActivity() {
        rule.getScenario().onActivity(activity -> { (2)
            // your asserts
        }); (3)
    }

}
1 Create a scenario, for every test call it will create a new Scenario, which will start the Activity.
2 This is not needed, but to be able to access the activity class, validation and calls to the activity needs to be done here.
3 Set an debug point here, this will allow keeping the activity open.

By running this test, the emulator should start the MainActivity and close it right away.

The ActivityScenarioRule also enables also following functionalities:

  • rule.getScenario().moveToState change the current state of the Activity

  • rule.getScenario().recreate recreate the Activity

By using the Debug mode, it is really helpful to set an breakpoint at the end of the test and start a specific activity without changing the AndroidManifest.xml file.

Clicking buttons

To be able to click a button, first it the button has to be found. Expresso provides the onView function to find an object.

// find by R.id
Espresso.onView(R.id.element_id);

// using a matcher
Espresso.onView(ViewMatchers.withText("text"));
Espresso.onView(ViewMatchers.withContentDescription("text"));

Actions can be performed on the result with the function perform().

// run click action
Espresso.onView(R.id.element_id).perform(ViewActions.click());

Show GUI hierarchy

The Gui hierarchy can be printed if an objected is selected, which can not be found. The hirarchy will show up in the log as exception.

Espresso.onView(ViewMatchers.withText("XYZ")).perform(ViewActions.click());

Using Edit Text

If advanced actions are needed, the UIAnimator is bringing those features. Including to wait for an Object to appear, pressing buttons, ec.

Add UIAnimator

app/build.gradle
dependencies {
    // ...
    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
}

Using the UIAnimator

To be able to use the UIAnimator, we first have to create an UiDevice, It is recommended to make it a class attribute in the test:

UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

Next, like with the Espresso, we have to select our target components:

// select by description
UiObject byDescription = device.findObject(new UiSelector().description("message_box"));

// select by R.ID
UiObject2 byRId = device.findObject(By.res(
        "ch.amk.exercise4.mqtt", (1)
        "message_box" (2)
));

// select by content, also has textContains for some part of content, textMatches for regex, ...
UiObject byContent = device.findObject(new UiSelector().text("Message"));
1 The package name of the activity
2 The R.id, sadly R.id.message_box does not work.

Basic JUnit assertions can then be used to test some things:

UiObject element = device.findObject(new UiSelector().description("message_box"));

// check if exists
assertTrue(element.exists());

Enter text into EditText

This can be done with Espresso or UIAnimator

this.rule.getScenario().onActivity(activity -> {
    // ViewActions do not work here
});

// change text with espresso
onView(withId(R.id.message_box))
    .perform(replaceText("Hello World"));

// change text with UiAnimator
device.findObject(By.res("ch.amk.exercise4.mqtt", "message_box"))
        .setText("Hello World");

Further

Further checkout those android testing samples, they contain more ideas on how tests can be executed. Also exercise 3 contains some CRUD tests combined with the recycling view, which also uses Dagger2 to mock backend services cleanly in the activity.