UI-Tests
In der klassischen Programmierung wird die Codebase hauptsächlich mit Unit- und Integration-Tests abgedeckt.
Das sollte man auch bei der Programmierung für Android so handhaben. In diesem Beitrag wird jedoch das
Sicherstellen eines fehlerfreien User Interfaces (UI) fokussiert.
Bei Android kann man UI-Tests nativ mithilfe des Frameworks Espresso abbilden. Daneben sind aber auch andere
Test Frameworks wie Robotium zu nennen, mit denen ebenfalls die Erstellung von UI-Tests für Android möglich
ist.
Espresso
Espresso ist ein Framework von Google, welches speziell für das Testen von Android User Interfaces entwickelt
wurde. Es wurde im April 2013 bei der Google Test Automation Conference vorgestellt und im Oktober des
selben Jahres für Entwickler zugänglich gemacht. Espresso ermöglicht es, native UI-Tests zu schreiben und so
direkt auf Code der Entwicklung zuzugreifen.
Setup
Da es sich bei Espresso um ein natives Framework handelt, ist die Einrichtung vergleichsweise einfach. Im
ersten Schritt müssen zunächst die neuesten Versionen der folgenden Dependencies zur build.gradle der
Applikation hinzugefügt werden:
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
Außerdem muss der Testrunner in der defaultConfig Sektion der build.gradle ergänzt werden:
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
Anschließend ist das Setup auch schon abgeschlossen und es kann mit dem Schreiben der Tests begonnen
werden.
Tests mit Espresso
In diesem Beispiel soll eine kleine Application getestet werden, die aus zwei Screens besteht. Der erste
Screen enthält zwei Buttons, und eine TextView, der zweite Screen enthält eine RecyclerView und einen
FloatingActionButton (FAB). Über der Liste soll eine TextView stehen.
Für die Umsetzung der Screens wurden zwei Fragments in einer Activity verwendet.
Die Liste wurde mithilfe
einer RecyclerView erstellt.
Klickt man im ersten Screen auf den linken Button, so wird der Counter in der TextView hochgezählt.
Abbildung 1: Ein Klick auf den rechten Button öffnet den zweiten Screen
Abbildung 2: Im zweiten Screen soll bei Klick auf den FAB ein Element zur Liste
hinzugefügt werden. Ein Element der Liste besteht aus einer TextView und einer Checkbox
In der TextView soll das Element mit Nummer stehen also “Item 0” beim ersten Element, “Item 1” beim zweiten
Element usw. Außerdem soll in der TextView über der Liste die Anzahl der Elemente, die ausgewählt sind und
die Anzahl der gesamten Elemente angezeigt werden. Das soll wie folgt dargestellt werden: “2/5 elements
selected”. Nach Klick auf den „Back“ Button oben links kehrt die Ansicht zum ersten Screen zurück.
Abbildung 3
Für diese Applikation werden nun UI-Tests definiert. Der erste Schritt hierfür ist das Anlegen einer Rule für
die verwendete Activity.
public class MainActivityTestRule extends ActivityTestRule{
public MainActivityTestRule(Class activityClass) {
super(activityClass);
}
}
Es kann hier auch die Standard ActivityTestRule verwendet werden. Da aber später an dieser Stelle noch etwas
überschrieben wird, soll eine eigene Rule für die MainActivity angelegt werden.
Im Anschluss kann nun der erste Espresso Test geschrieben werden. Hierfür legt man eine Klasse im
“androidTest” Ordner an, in welchem standardmäßig nach Integration Tests gesucht wird. Da keine extrem große
Testklasse benötigt wird, kann eine Klasse für den ersten Screen und eine für den zweiten Screen erstellt
werden. Damit die Rule in beiden verwendet werden kann, legt man die “BaseTest“ Klasse an, von der die
beiden Testklassen erben können. Anschließend kann eine neue Instanz der Rule angelegt werden:
@Rule
public MainActivityTestRule mActivityRule = new MainActivityTestRule<>(MainActivity.class);
Im nächsten Schritt kann eine Testklasse für den ersten Screen angelegt werden. Diese erbt von „BaseTest“.
Mit der Annotation @Test
wird nun eine Methode markiert, die einen Test
repräsentiert. Es empfiehlt sich,
die Testmethoden aussagekräftig zu benennen, damit direkt ersichtlich ist, was genau getestet wird. Bei
UI-Tests empfiehlt sich folgende Notation:
test_wasSollImTestPassieren_WasWirdErwartet()
Dieses Vorgehen kann dazu führen, dass die Namen der Testmethoden sehr lange werden, aber erlaubt es dem
Entwickler auch ohne Dokumentation schnell einzusehen, was in dieser Methode getestet wird.
Im ersten Test soll überprüft werden, ob die UI mit den richtigen Werten initialisiert wurde:
- Im Textfeld wird der korrekte Inhalt angezeigt
- Beide Buttons haben das passende Label
@Test
public void test_uiHasSetupCorrectly_AllViewsShouldBeVisibleAndHaveCorrectValues(){
onView(withId(R.id.fragment_button_counter_text_view)).check(
matches(withText("Number of Button Clicks: 0")));
onView(withId(R.id.fragment_button_counter_button)).check(matches(withText("Count")));
onView(withId(R.id.fragment_button_fragment_change_button)).check(
matches(withText("Change Fragment")));
}
Mit onView()
wird die View selektiert, die getestet werden soll. Die id
ist die “android:id”, die im XML
Layout File für ein UI-Element festgelegt wird. Anschließend wird über check()
ein Matcher erwartet. In
unserem Fall wird jeweils nur der String verglichen.
Hier sollten besser String Resources anstelle von festen Strings verwendet werden. Dies hat zwei
Vorteile:
- Eine Änderung betrifft sofort den Test und die Applikation
- Sollte die Applikation übersetzt werden kann man die Tests unabhängig von der Gerätesprache ausführen
Für die Testapplikation empfiehlt es sich aber, bei festen Strings zu bleiben.
Nun ist sichergestellt, dass der Screen ordentlich aufgebaut wurde. Die nächste Funktion des ersten Screens
ist das Klicken auf den linken Button und das Hochzählen des Counters im Textfeld. Neben der Überprüfung
eines Elementes wie im vorangegangen Test, kommt hier außerdem das Klicken des Buttons, also eine Action
hinzu:
@Test
public test_ButtonClickUpdatesCounter_CounterCountsUp(){
onView(withId(R.id.first_screen_text_view)).check(matches(withText(“0 Klicks”)));
onView(withId(R.id.first_screen_left_button)).perform(click());
onView(withId(R.id.first_screen_text_view)).check(matches(withText(“1 Klicks”)));
}
Zunächst wird geprüft, ob der Anfangswert korrekt ist, anschließend wird über die onView(withId())
der linke
Button ausgewählt und über perform()
eine Action ausgewählt, die auf dem
Objekt ausgeführt werden soll. In
unserem Fall ist die Action click()
.
Anschließend sollte kontrolliert werden, ob der Counter hochgezählt wurde. Auf eine Unterscheidung zwischen
Singular und Plural bei einem oder mehreren Elementen wurde hier verzichtet. Um sicherzustellen, dass dies
auch mehr als einmal funktioniert, wird ein weiterer Test hinzugefügt, in welchem der Button zehn Mal
geklickt wird:
@Test
public test_ButtonClickUpdatesCounter_CounterCountsUp(){
onView(withId(R.id.first_screen_text_view)).check(
matches(withText(“0 Klicks”)));
for(int i = 0; i<10 ; i++{
onView(withId(R.id.first_screen_left_button)).perform(click());
}
onView(withId(R.id.first_screen_text_view)).check(
matches(withText(“10 Klicks”)));
}
Die click()
Action wird nun in einer Schleife zehn mal hintereinander
ausgeführt. Im Anschluss erfolgt die
Überprüfung, ob korrekt mitgezählt wurde.
Es gibt im ersten Screen noch eine zweite Action, deren korrekte Ausführung mittels eines UI-Tests
sichergestellt werden soll. Durch Klick auf den rechten Button soll sich der zweite Screen öffnen.
Um zu testen, ob sich die View tatsächlich geöffnet hat, kann geprüft werden, ob ein Layout Element des
zweiten Screens sichtbar ist. Man könnte alternativ auch den Titel der Toolbar oder ein anderes
Alleinstellungsmerkmal der View verwenden. In unserem Fall wurde der FAB im zweiten Screen ausgewählt:
@Test
public test_ButtonClickUpdatesCounter_CounterCountsUp(){
onView(withId(R.id.first_screen_right_button)).perform(click());
onView(withId(R.id.second_screen_floating_action_button)).check(matches(isDisplayed()));
}
Nach Klick auf den Button im ersten Screen kann der FAB über die Layout-ID gefunden werden und es wird
überprüft, ob der Button angezeigt wird.
Nun sind alle Funktionen des ersten Screens abgedeckt. Die Tests für den zweiten Screen werden in der zweiten
Testklasse angelegt. Hierbei fällt auf, dass alle Tests zunächst zum zweiten Screen navigieren müssen, bevor
sie ausgeführt werden können.
Da dies für alle Tests in dieser Testklasse gilt, kann hier die @Before
Annotation verwendet und vor jedem
Test ausgeführt werden. Andernfalls müsste in jedem Test eine Methode aufgerufen werden, die zum zweiten
Screen navigiert.
@Before
public void setUp(){
onView(withId(R.id.first_screen_right_button)).perform(click());
}
Nun kann mit der Prüfung des zweiten Screens begonnen werden, folgendes sollte getestet werden:
- Die UI ist im Grundzustand korrekt aufgebaut worden.
Ein Klick auf den FAB fügt ein Element zur Liste hinzu, die TextView in dem hinzugefügten Element enthält den
korrekten Text und die TextView über der Liste hat sich korrekt aktualisiert.
- Das Anwählen einer Checkbox aktualisiert die TextView über der Liste.
- Das Anwählen einer Checkbox und anschließende Hinzufügen eines Items lässt die Checkbox angewählt und
die TextView über der Liste wird korrekt aktualisiert.
- Das Abwählen einer Checkbox und anschließende Hinzufügen eines Items lässt die Checkbox abgewählt und
die TextView über der Liste wird korrekt aktualisiert.
Um die UI im Grundzustand zu überprüfen, muss hier nur der Inhalt der TextView über der Liste und die
Sichtbarkeit des FAB überprüft werden.
Nun zum ersten Test, der ein Element einer RecyclerView enthält. Der Klick auf den FAB kann analog zu den
Klicks auf die Buttons im ersten Screen realisiert werden. Um ein Element einer RecyclerView zu untersuchen,
benötigt man einen eigenen ViewMatcher. Durch eigene ViewMatcher kann man in Espresso nahezu jedes Element
untersuchen. Oft gibt es bereits Implementierungen von ViewMatchern für UI Elemente. Für die RecyclerView
wurde
ViewMatcher verwendet.
Anschließend wird in der „BaseTest“ Klasse eine Methode withRecyclerView()
eingeführt, um den Watcher wie die
Standard Espresso Matcher aufrufen zu können.
public static RecyclerViewMatcher withRecyclerView(final int recyclerViewId) {
return new RecyclerViewMatcher(recyclerViewId);
}
Nun können die Elemente der RecyclerView getestet werden. Hierbei sollte man sich angewöhnen, vor dem Testen
eines Elements zu dem Index des Elements zu scrollen. Dies ist notwendig, da Espresso nur ein Element testen
kann, das auf dem Bildschirm angezeigt wird ist. Um das Scrollen der RecyclerView möglich zu machen, müssen
wir nun auch das “espresso-contrib” Repository einbinden. Dieses enthält zusätzliche Möglichkeiten
UI-Elemente, zu matchen oder mit ihnen zu interagieren. Wir benötigen für unseren Test diese
RecyclerViewActions:
@Test
public void test_clickFAB_itemIsAddedToListTextViewUpdated() {
onView(withId(R.id.list_fragment_floating_action_button)).perform(click());
onView(withId(R.id.list_fragment_recycler_view)).perform(RecyclerViewActions.scrollToPosition(0));
onView(withRecyclerView(R.id.list_fragment_recycler_view).atPosition(0)).check(
matches(hasDescendant(withText("Item 0"))));
onView(withId(R.id.list_fragment_title_text_view)).check(matches(withText("0/1 items selected")));
}
Nach dem Klick auf den FAB scrollt man mithilfe der RecyclerViewAction scrollToPosition(index)
zum Index der
Liste, der überprüft werden soll. Dies ist hier zwar nicht unbedingt notwendig, da das erste Element der
List im Normalfall immer sichtbar ist, aber so wird der Schritt auch in anderen Fällen nicht vergessen.
Zusätzlich sollte man bedenken, dass das Gerät, mit welchem man entwickelt, nicht zwingend das gleiche Gerät
ist, auf dem die Tests ausgeführt werden müssen.
Anschließend kann mithilfe des Matchers auf das Listen Element an der Stelle 0 zugegriffen und überprüft
werden, ob eine ihrer Subviews den korrekten Text enthält. Da hier nur ein Element mit dem entsprechenden
Text vorgesehen ist, reicht das aus.
Als nächstes soll eine Checkbox angewählt und im Anschluss überprüft werden, ob der Text über der Liste sich
korrekt ändert:
@Test
public void test_addElementSelectElementCheckBox_shouldRefreshTextViewCorrectly() {
onView(withId(R.id.list_fragment_floating_action_button)).perform(click());
onView(withId(R.id.list_fragment_recycler_view)).perform(RecyclerViewActions.scrollToPosition(0));
onView(withId(R.id.list_fragment_recycler_view)).perform(
RecyclerViewActions.actionOnItemAtPosition(0, clickChildViewWithId(R.id.row_list_item_checkbox)));
onView(withId(R.id.list_fragment_title_text_view)).check(matches(withText("1/1 items selected")));
}
Hier kommt als Neuerung der Klick auf die Checkbox hinzu. Da sich die Checkbox jeweils in einem Listenelement
befindet, kann man hier wieder eine RecyclerViewAction verwenden. Mit actionOnItemAtPosition
kann ein Index
und eine ViewAction mitgegeben werden:
public class ChildClickViewAction {
public static ViewAction clickChildViewWithId(final int resourceId) {
return new ViewAction() {
@Override
public Matcher getConstraints() {
return null;
}
@Override
public String getDescription() {
return "Click on a child view with specified id.";
}
@Override
public void perform(UiController uiController, View view) {
View v = view.findViewById(resourceId);
v.performClick();
}
};
}
}
In diesem Fall wird über die ResourceId das entsprechende Element innerhalb der gegebenen View gesucht und
auf diese View geklickt. Nach dem Klick auf die Checkbox wird überprüft, ob sich die TextView über der Liste
auch entsprechend aktualisiert hat.
Für die letzten Tests werden keine neuen Konzepte mehr benötigt, daher werden sie an dieser Stelle nicht
explizit aufgelistet. Beide Tests sind jedoch über Github einsehbar.
Abschließend soll noch geprüft werden, ob die Navigation über den „Zurück“-Button oben links funktioniert.
Hierbei ist auf eine Besonderheit zu achten: Leider kann Espresso nicht mit der Id auf den Button zugreifen.
Zumindest dann nicht, wenn dieser mit setHomeAsUpEnabled(true)
erstellt
wurde. Stattdessen wird hierfür eine
Description verwendet:
@Test
public void test_pressBackButton_shouldReturnToButtonFragment(){
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
onView(withId(R.id.fragment_button_counter_text_view)).check(matches(isDisplayed()));
}
Um also auf den „Zurück“-Button zu klicken, muss zunächst die Description ermittelt werden. Diese hat eine
feste Resource Id, d.h. dieser Test funktioniert auch sprachunabhängig. Im letzten Schritt wird geprüft, ob
die TextView im ersten Screen angezeigt wird.
Nun wurde die korrekte Funktionsweise der App fast vollständig mit UI-Tests sichergestellt. Ein spezieller
Fall sollte jedoch noch berücksichtigt werden. Bisher würden die Tests nur laufen, wenn das Testgerät
entsperrt ist. Bei ausgeschaltetem Bildschirm oder gesperrten Gerät schlagen alle Tests fehl, da die
Activity nicht gefunden wird. Daher soll das Gerät entsperrt und den Bildschirm angeschaltet werden, bevor
die Tests ausgeführt werden. Hierfür darf das Testgerät keine Sperre wie eine PIN besitzen.
Hierfür müssen wir wider in unser MainActivityTestRule
Klasse zurück und
das Interface
ActivityLifecycleCallback
implementieren.
Dies ermöglicht es uns die Methode onActivityLifecycleChanged()
zu
implementieren.
@Override
public void onActivityLifecycleChanged(Activity activity, Stage stage) {
if (stage == Stage.PRE_ON_CREATE) {
activity.getWindow()
.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
}
}
Hier prüft man, ob sich die Activity im PRE_ON_CREATE
Status, also bevor
sie erstellt wird, befindet und
versucht, den Bildschirm anzuschalten und das Gerät zu entsperren.
Zusätzlich muss die Methode beforeActivityLaunched
überschrieben werden,
um den Listener zu setzen. So werden
die Tests auch bei ausgeschaltetem Bildschirm ausgeführt.
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(this);
}
Abschließend lässt sich festhalten, dass Espresso ein wertvolles Tool bei der App-Entwicklung für Android
ist, dessen Nutzung im Zusammenspiel mit anderen Testarten sehr zu empfehlen ist.
Die gesamte Applikation inklusive aller hier beschriebenen Tests steht auf GitHub zur Verfügung.