Neben den Unit Tests gibt es noch eine weitere Form der automatisierten Tests, die User Interface Tests, kurz
UI Tests. Während
Unit Tests sich wirklich auf eine abgegrenzte Einheit oder Funktion konzentrieren, simulieren UI Tests das
tatsächliche
Verhalten eines Nutzers, der mit der App interagiert. Dadurch sind allerdings auch deutlich mehr Dinge zu
beachten, damit UI
Tests genau wie Unit Tests deterministisch sind, also bei jeder Durchführung zum gleichen Ergebnis
führen.
Dies lässt sich gut am Beispiel einer iOS-App zeigen. In dieser Beispiel-App gibt es drei Funktionen, die
sich alle mit UI Tests
testen lassen. Wird ein Text eingegeben, erscheint dieser nach Bestätigung in der Navigation Bar. Drückt man
den Button,
erscheint ein Alert mit dem Titel „Good Morning“. Zuletzt lässt sich über den Switch einstellen, ob man den
Button drücken kann
oder nicht.
Der erste UI-Test
Zum Schreiben eines UI Tests kann wie bei Unit Tests auf Apples XCTest Framework zurückgegriffen werden. Im
Gegensatz zu Unit
Tests muss man in jedem Test Case allerdings die App starten und beschreiben, wie mit welchen Elementen
interagiert werden
soll.
Das Starten der App geschieht folgendermaßen:
let app = XCUIApplication()
app.launch()
Auf die einzelnen UI-Elemente lässt sich nun über die Variable app zugreifen. Der Button im obigen Beispiel
lässt sich
beispielsweise über den Titel auslesen.
let button = app.buttons["Show Alert"]
Der Nachteil dieser Art und Weise ist allerdings, dass man den UI Test aktualisieren müsste, wenn sich aus
gewissen Gründen der
Titel des Buttons ändern würde. Um dies zu vermeiden, lässt sich auf jedem UIKit-Element der
accessibilityIdentifier setzen, der
genau für UI Tests gedacht ist. Dies kann entweder im Code geschehen oder man setzt ihn im Storyboard bzw.
Xib-File im Identity
Inspector der jeweiligen View. Analog dazu gibt es in SwiftUI den Modifier .accessibility(identifier:).
Gibt man dem Button nun den accessibilityIdentifier „Example.Button“, kann im UI Test über diesen String
darauf zugegriffen
werden.
let button = app.buttons["Example.Button"]
Um gewisse Bedingungen zu testen verwendet man, wie auch in Unit Tests, die Assertions, welche XCTest
anbietet. Damit
sichergestellt wird, dass der Button gedrückt werden kann, kann man wie folgt vorgehen:
XCTAssertTrue(button.isEnabled)
Im letzten Schritt unseres ersten UI Tests soll geprüft werden, dass der Button nach Deaktivieren des
Switches nicht mehr
gedrückt werden kann. Dazu wird der Switch anhand seines vergebenen accessibilityIdentifier ausgelesen und
einmal betätigt.
Schließlich folgt die Prüfung, ob der Button nicht mehr aktiv ist.
let uiSwitch = app.switches["Example.Switch"]
uiSwitch.tap()
XCTAssertFalse(button.isEnabled)
Xcode bietet auch die Möglichkeit an, bestimmte Interaktionen, die getestet werden sollen, aufzunehmen. Das
bedeutet, man kann im
Simulator mit UI-Elementen interagieren und der Recorder übersetzt dies in Code für einen UI Test. In den
meisten Fällen ist
dies nicht besonders zuverlässig und der Code ist meist komplizierter als notwendig, es kann jedoch als
Einstiegshilfe dienen.
Eine Aufnahme kann über den roten Record-Button unterhalb des Editors gestartet werden, wenn sich der Cursor
in einer
Testmethode befindet.
Alerts und Navigation Bar
Nach der Fertigstellung des ersten UI Tests können wir nach dem gleichen Muster auch für unsere beiden
weiteren Szenarien
vorgehen.
Um zu testen, dass der Button ein Alert auslöst, wird dieser wieder ausgelesen und schließlich über die
tap()-Methode gedrückt.
Ein Alert lässt sich mittels seines Titels auslesen, allerdings kann man hier auch auf die element-Property
zurückgreifen, die
man stets nutzen kann, wenn es nur ein Element des entsprechenden Typs gibt. Wenn z. B. nur ein Button auf
dem Screen zu sehen
ist, könnte man auch über app.buttons.element auf den Button zugreifen. Ähnlich funktioniert auch die
firstElement property, die
das erste passende Element zurück liefert, wenn es beispielsweise keine Rolle spielt, welches konkrete
Element des Typs benötigt
wird.
Nun können wir mittels der exists-Property sicherstellen, dass das Alert auch wirklich angezeigt wird und
schließlich über den
OK-Button wieder schließen. So sieht das ganze am Ende so aus:
let app = XCUIApplication()
app.launch()
let button = app.buttons["Example.Button"]
button.tap()
let alert = app.alerts.element
XCTAssertTrue(alert.exists)
alert.buttons["OK"].tap()
Im letzten UI Test soll sichergestellt werden, dass der Text, welcher in das Textfeld eingegeben wird, nach
Bestätigung in der
Navigation Bar angezeigt wird. Auch Textfelder lassen sich über die app-Variable auslesen. Bei der Eingabe
des Texts sind ein
paar Dinge zu beachten. Zunächst muss das Textfeld den Fokus erhalten, indem wir es mittels tap() auswählen.
Daraufhin lässt
sich mit der typeText()-Funktion ein beliebiger String eingeben. Damit der Text in die Navigation Bar
geschrieben wird, muss der
Return-Button der Tastatur betätigt werden. An der Stelle ist es im UI Test extrem wichtig, im Simulator das
Software Keyboard
aktiviert zu haben (I/O —> Keyboard —> Toggle Software Keyboard), da sonst der Return-Button nicht gefunden
werden kann.
Die Navigation Bar kann schließlich ebenfalls über ihren Titel ausgelesen werden, woraufhin geprüft wird, ob
diese auch wirklich
vorhanden ist.
let app = XCUIApplication()
app.launch()
let textField = app.textFields["Example.TextField"]
textField.tap()
textField.typeText("Good Morning")
app.keyboards.buttons["Return"].tap()
let navigationBar = app.navigationBars["Good Morning"]
XCTAssertTrue(navigationBar.exists)
Stolpersteine
Ein wichtiger Punkt, der bei UI Tests zu beachten ist, ist Flakiness. Man spricht davon, dass ein Test flaky
ist, wenn er
manchmal erfolgreich durchläuft, ein anderes Mal aber nicht. Dies kann viele Gründe haben. Ein Grund kann
sein, dass ein State
aus einem vorherigen Test nicht zurückgesetzt wurde und die App nicht im korrekten Zustand ist, sodass z. B.
ein Onboarding
Screen, der nur beim ersten App Start angezeigt wird, beim 2. Lauf des UI Tests nicht mehr angezeigt
wird.
Eine gängige Art und Weise, dies zu vermeiden, sind Launch Arguments. Diese können der App vor dem Start im
UI-Test mitgegeben
und im Code der App ausgelesen werden. So kann z. B. auf diesem Weg definiert werden, dass wir die App bei
jedem Durchlauf
zurücksetzen möchten:
let app = XCUIApplication()
app.launchArguments = ["reset-app"]
app.launch()
Im AppDelegate oder SceneDelegate kann daraufhin geprüft werden, ob das reset-Argument vorhanden ist und dann
gegebenenfalls die
Datenbank oder UserDefaults zurückgesetzt werden, je nach App Szenario.
if CommandLine.arguments.contains("reset-app") {
resetApp()
}
Im Gegensatz zu Unit Tests, bei welchen Netzwerkaufrufe meist nicht wirklich ausgeführt werden, werden diese
in UI Tests ganz
normal ausgeführt. Das kann allerdings ebenfalls dazu führen, dass Elemente, die z. B. erst nach Laden der
Daten vom Server
angezeigt werden, im UI Test nicht direkt verfügbar sind.
Wird also ein Button erst angezeigt, nachdem eine Anfrage an den Server erfolgreich durchgeführt wurde, kann
dies im UI Test
mittels eines Delays berücksichtigt werden. Über die Methode waitForExistence kann ein Timeout definiert
werden, der im Grunde
aussagt, dass die Dauer des Timeouts abgewartet wird, dass das Element existiert. Erst wenn nach Ablauf des
Timeouts die
Bedingung nicht erfüllt ist, gilt der Test als fehlgeschlagen.
XCTAssertTrue(timelineCell.waitForExistence(timeout: 2))
Launch Arguments und die waitForExistence-Methode sind sehr nützlich, um sicherzustellen, dass UI Tests
deterministisch sind.
Erst, wenn jeder Durchlauf des Tests zum gleichen Ergebnis führt, ist er wirklich von Nutzen.
Wrapping Up
Wie gezeigt wurde, sind UI Tests ein sehr hilfreiches Mittel zur Qualitätssicherung einer App. Dabei können
zum einen bestimmte
Workflows getestet werden, aber es lässt sich auch sicherstellen, dass die UI auf verschiedensten
Gerätegrößen gut aussieht.
Zudem lässt sich im Grunde jede Interaktion und Geste des Benutzers abbilden. Lediglich WebViews oder
Szenarien, bei welchen auf
die Sensoren des Geräts zugegriffen werden, sind weniger geeignet.
Da es mehr Aufwand benötigt, UI Tests zu schreiben und durchzuführen als z. B. Unit Tests, sollte man ein
gewisses Gefühl dafür
entwickeln, an welchen Stellen der Einsatz Sinn macht.