Testing neu erfunden?

Android UI Testing mit dem Espresso-Testkit

Lars Röwekamp, Arne Limburg
© S&S Media

„If you don’t test Android, Android tests you!“. Treffender als Valera Zakharov, Software Engineer bei Google, im Rahmen seines GTAC 2013 Talks, kann man die Relevanz von Tests – ja, dazu zählen auch UI-Tests – wohl kaum formulieren. Wie passend, dass er gleich sein eigenes Test-Kit im Gepäck hatte, das mittlerweile als Open-Source-Lösung zur Verfügung steht: Espresso.

Einen verlässlichen Test in Android zu schreiben, sollte nicht länger dauern, als sich einen einfachen Espresso zu ziehen. Soweit die Theorie. In der Realität dagegen hat man bei vielen Testtools nicht selten das Gefühl, einen Double-Shot-Caramel-Sauce-Upside-Down-Single-Whip-Half-Decaf-Latte (Zitat Zakharov) zuzubereiten: extrem kompliziert und selten zweimal das gleiche Ergebnis. Genau hier setzt das neue Testframework Espresso von Google an und stellt drei grundlegende Paradigmen – „easiness“, „reliability“ und „durability“ – als wesentliche Treiber in den Fokus. Das Framework soll Entwickler mit einfachsten Mitteln in die Lage versetzen, UI-Tests zu schreiben, die immer das gleiche Ergebnis liefern und dabei auch Änderungen am UI, wie zum Beispiel geänderte Buttontexte, problemlos überleben.

Lesetipp: Im Java Magazin 5.14 findet sich ein weiterer, detaillierter Artikel über Espresso von Danny Preussler. Ein Blick lohnt sich!

Easiness

Was macht einen guten UI-Test aus? Er simuliert genau das Verhalten, das auch ein realer Benutzer an den Tag legen würde: UI-Element auswählen, Aktion auf dem Element ausführen und abschließend das erwartete Ergebnis prüfen. Genau diese drei Elemente finden sich auch im Espresso-API in Form von Floating-API-Methoden der Klasse Espresso wieder:

• auswählen: onView(Matcher<View>)
• ausführen: perfom(ViewAction)
• prüfen: check(ViewAssertion)

Die folgenden beiden Codezeilen würden zum Beispiel einen Button mit der Resource-ID btn_toggle_text auswählen, ihn klicken und im Anschluss prüfen, ob sich der Inhalt eines Textfelds mit der Resource-ID txt_toggle auf den Wert der String-Ressource toggle geändert hat.

onView(withId(R.id.btn_toggle_text)).perfom(click()); 
onView(withId(R.id.txt_toggle_text)).check(matches(getString(R.string.toggle));

Für alle drei Methoden stellt Google gleich eine ganze Reihe vorgefertigter Helferlein zur Verfügung, die den Testautoren passende Hamcrest-Matcher, ViewActions oder ViewAssertions liefern. Bei den Matchern dürften vor allem die Varianten withId, withText, withContentDescription, isDisplayed, hasFocus und hasSibling von Interesse sein. Als ViewActions stehen unter anderem click, longClick, doubleClick, typeText und scrollTo zur Verfügung. Scheinbar etwas spärlich sieht es bei den ViewAssertions aus. Hier trügt allerding der Schein, da ein sehr flexibles matches nahezu jede denkbare Prüfung erlaubt und daher als Spezialfall nur noch ein doesNotExist notwendig ist.

Reliablility

Was aber macht sonst noch einen guten UI-Test aus? Er liefert in jedem Durchlauf dasselbe Ergebnis und fordert dazu von den Testautoren keine Aktionen, die nicht auch der reale Benutzer ausführen würde. Hier wird es schon etwas komplizierter, denn die meisten Testframeworks bieten zusätzliche Calls zum gezielten Ansprechen einer Activity oder View (z. B. getActivity(…), getView(…)) bzw. zur Manipulation des App-Lifecycles (z. B. sleep(…), waitUntil(…), Boilerplate-Code) an, um so innerhalb der Tests das Verhalten eines realen Devices besser simulieren zu können. Leider führen aber genau diese Methoden gerne einmal zu einem nicht deterministischem Verhalten, da sie den eigentlichen Programmablauf und den UI-Stack verfälschen.
Der Trick von Espresso ist, dass genau diese manipulierenden Methoden nicht zur Verfügung gestellt werden. Stattdessen wird durch das Framework und den internen Testablauf stets ein konsistenter Zustand garantiert. Nehmen wir als Beispiel einmal ein Thread.sleep(…) oder das oben erwähnte waitUntil(…). Anstatt durch Boilerplate-Code innerhalb des Tests mit großer Wahrscheinlichkeit – aber eben nicht mit hundertprozentiger Sicherheit – einen Zeitpunkt zu erwischen, in dem der Test ungehindert auf den UI-Thread zugreifen kann, wartet das Espresso-Framework bewusst für jede Test-Action so lange, bis die Anwendung im Zustand „idle“ ist und somit der UI-Thread gefahrlos und exklusiv durch den Test genutzt werden kann. Was sich zunächst wie eine künstliche Restriktion anhört, hat bei Google-internen Messungen zu einer deutlichen Beschleunigung der Tests geführt. In der Regel sorgen die vielen Sleep-Anweisungen bzw. künstlichen Thread-Syncs innerhalb der Tests nämlich für deutlich mehr Verzögerung als das gezielte Warten auf einen günstigen Moment zur Testausführung.

Durability

Da wir es bei Espresso-Tests mit Android Instrumentation zu tun haben, lassen sich UI-Tests in der Regel so schreiben, dass sie unerwartete UI-Änderungen überstehen bzw. diese bereits zur Compile-Zeit aufzeigen. Nehmen wir noch einmal als Beispiel den Test von oben, bei dem ein Button geklickt werden soll. Nehmen wir zusätzlich an, dass wir von dem Button die Beschriftung press me kennen. Ein möglicher Testcode mit Zugriff zur Simulation eines Buttonklicks könnte wie folgt aussehen:

onView(withText("press me")).perfom(click()); 
// ... test something 

Es wird allerdings schnell klar, dass dieser Test sofort Probleme mit sich bringt, sobald sich der Text der Buttonbeschriftung ändert. Eine bessere Lösung wäre also, statt des konkreten Texts press me die zugehörige String-Ressource der zu testenden Anwendung zur Lokalisierung des gewünschten Buttons zu verwenden.

onView(withText(getString(R.string.txt_press_me))).perfom(click()); 
// ... test something 

Ändert sich nun die Beschriftung des Buttons, indem die zugehörige String-Ressource abgeändert wird, funktioniert der Test auch weiterhin. Was aber ist, wenn eine andere String-Ressource für die Beschriftung herangezogen wird? Wird die alte String-Ressource gelöscht, signalisiert uns bereits der Compiler einen Fehler. Das ist genau das Verhalten, das wir uns wünschen. Ansonsten schlägt der Test allerdings erst zur Ausführungszeit fehl. Noch besser wäre somit folgende Variante des Tests:

onView(withId(R.id.btn_press_me)).perfom(click()); 
// ... test something 

Egal wie nun die Beschriftung geändert wird, der Test greift via Resource-ID immer auf den richtigen Button zu und führt mit ihm die gewünschte Aktion aus. Probleme bekommen wir nur noch dann, wenn der App-Entwickler die ID des Buttons umbenennt. Dies allerdings würde bereits vom Compiler abgefangen, sodass wir den Test problemlos anpassen können.

Fazit: Weniger ist mehr

Das Schreiben von UI-Tests mit Espresso ist denkbar einfach, was die üblicherweise vorhandene Hemmschwelle zur Umsetzung von Tests deutlich senken dürfte. Gleichzeitig können die Tests dank Instrumentation so aufgebaut werden, dass sie relativ zukunftssicher sind. Da weder manipulierender Boilerplate-Code noch künstliche Warteschleifen eingebaut werden, ist auch die Testausführung extrem schnell. Natürlich wird es immer mal wieder Situationen geben, in denen die vorgefertigten Matcher, ViewActions oder ViewAssertions nicht für die eigenen Anwendungsfälle ausreichen. Für diesen Fall sind entsprechende Extension Points vorgesehen, mit deren Hilfe eigene Varianten der drei Main-Player implementiert werden können.

Und auch auf den Ablauf der Testausführung kann Einfluss genommen werden. Während in der Regel das Framework, unter Zugriff auf den AsyncTask-Thread-Pool, von selbst feststellt, wann der optimale Zeitpunkt zum Ausführen der nächsten Test-Action ist, kann es in einzelnen Fällen dazu kommen, dass dies dem Framework bewusst signalisiert werden muss. Dies ist zum Beispiel dann der Fall, wenn ein eigenes Threading implementiert wurde. Für diesen speziellen Fall kann eine entsprechende Ressource, die das IdleResoruce-Interface implementiert via registerIdlingResource im Test-Set-up registriert werden.

Um möglichst viel Feedback von den Anwendern des Testframeworks zu bekommen, sammelt Google im Hintergrund während der Testausführungen einiges an Informationen. Diese Option kann von den Anwendern jederzeit ausgestellt werden, ist allerdings per Default aktiviert. Es ist davon auszugehen, dass das gesammelte Feedback direkt in die kommenden Iterationen des Testframeworks einfließen wird. In diesem Sinne: Stay tuned …

Geschrieben von
Lars Röwekamp
Lars Röwekamp
Lars Röwekamp ist Gründer des IT-Beratungs- und Entwicklungsunternehmens open knowledge GmbH, beschäftigt sich im Rahmen seiner Tätigkeit als „CIO New Technologies“ mit der eingehenden Analyse und Bewertung neuer Software- und Technologietrends. Ein besonderer Schwerpunkt seiner Arbeit liegt derzeit in den Bereichen Enterprise und Mobile Computing, wobei neben Design- und Architekturfragen insbesondere die Real-Life-Aspekte im Fokus seiner Betrachtung stehen. Lars Röwekamp, Autor mehrerer Fachartikel und -bücher, beschäftigt sich seit der Geburtsstunde von Java mit dieser Programmiersprache, wobei er einen Großteil seiner praktischen Erfahrungen im Rahmen großer internationaler Projekte sammeln konnte.
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: