Blog
iOS UI Testing und warum es nicht immer funktioniert: auch bekannt als "Pushing The Limits of the XCTest Framework

XCTest UI-Tests
Als Apple 2015 das neue UI-Automatisierungs-Framework vorstellte, waren wir begeistert. Endlich konnten wir die Benutzeroberfläche und das Verhalten unserer Anwendungen mit Swift (oder Objective-C) und der nativen Bibliothek testen.
Früher mussten wir Skripts in JavaScript schreiben und sie mit Instruments ausführen. Das war nicht der einfachste Weg, um Tests zu schreiben, zu pflegen und zu debuggen. Eine andere Möglichkeit war die Verwendung des KIF-Frameworks, mit dem wir Integrationstests schreiben und die Benutzeroberfläche der Anwendung testen können. Ein großer Nachteil dabei ist die Verwendung der privaten API, die sich mit jeder iOS / Xcode Version ändern kann. Seit Apple das neue Framework eingeführt hat, haben sich die Dinge zum Glück geändert... aber haben sie das wirklich?
Wenn Sie anfangen, länger damit zu spielen, werden Sie feststellen, dass das Verhalten einiger Methoden je nach iOS- und/oder Xcode-Version unterschiedlich ist. Aber das ist etwas, an das sich Apple schon seit einiger Zeit gewöhnt hat. Allerdings fangen die wirklich ausgefallenen Dinge erst jetzt an, zu erscheinen. Wenn Sie es endlich schaffen, funktionierende UI-Tests zu schreiben und die magische Zahl von 128 Testfällen erreichen, werden Sie das erste Osterei finden.
Pseudo Terminal Limit
Am Anfang werden Sie nichts bemerken. Starten Sie Xcode, öffnen Sie das Projekt, führen Sie Tests aus, und alles funktioniert wie erwartet. Aber wenn Sie Fastlane, Travis CI, Jenkins, den Xcode Server oder ein anderes System zur kontinuierlichen Integration konfigurieren, um Ihr Projekt zu erstellen, werden Sie feststellen, dass nach etwa Test 128 alle Tests fehlschlagen und der gleiche Fehler auftritt
Fehler Domain=IDEPseudoTerminalDomain Code=1 "(null)"
Dieses Problem ist sehr interessant, weil es in Xcode nicht auftritt. Es tritt nur auf, wenn Sie xcodebuild direkt ausführen (oder Wrapper wie Gym verwenden), so dass die Fehlersuche nicht ganz einfach ist.
Ich habe bei Google nach einer Lösung gesucht, bin aber leider nicht fündig geworden. Ich stieß nur auf Informationen zu einigen Problemen, die 2014 berichtet wurden, aber ohne Ergebnis. Auch das Zurücksetzen des iOS Simulators hat nicht geholfen. So kam ich auf die Idee, etwas zu recherchieren, genauer gesagt, in Xcode einzutauchen und zu versuchen, meine eigene Lösung zu finden.
Am Anfang hatte ich keine Ahnung, wie ich dieses Problem debuggen sollte. Alles funktionierte mit Xcode, so dass ich keine Haltepunkte oder einen Debugger verwenden konnte. Mein erster Ansatz bestand darin, nur die fehlgeschlagenen Tests mit Hilfe von -only-testing flag auszuführen. Das hat nicht geholfen. Es gelang mir, die Tests ohne Probleme auszuführen. Mein nächster Gedanke war, dass die Ausführungsreihenfolge etwas damit zu tun haben könnte. Auch das Deaktivieren von Tests aus dem Schema hat nicht geholfen. Nachdem ich 128 Tests erreicht hatte, fingen sie an, fehlzuschlagen. Nach ein paar Versuchen wurde ich müde und hatte das Gefühl, dass ich genug hatte. Wir hatten bereits fast 150 Tests geschrieben. Das Ausführen der gesamten Testsuite dauerte fast eine Stunde. Die Überprüfung einiger weniger Fälle hat mich den ganzen Tag gekostet.
Im nächsten Schritt wollte ich die Testausführung beschleunigen. Ich hoffte, dass dieses Problem nicht mit meinem Projekt zusammenhängen würde, also habe ich ein kleines Testprojekt erstellt. Diese einfache Anwendung besteht aus 200 UI-Tests, die nichts tun:
func testBeispiel000(){}
func testBeispiel001(){}
func testBeispiel002(){}
// ...
func testBeispiel199(){}
Ich führte die Tests aus und "glücklicherweise" schlugen sie fehl und es erschien die gleiche Fehlermeldung. Das war der Beweis dafür, dass dieses Problem nicht mit meinem Projekt zusammenhing, sondern mit dem Tool xcodebuild .
Danach habe ich angefangen, nach einer allgemeineren Lösung zu suchen. Wenn der Fehler mit einer Ressource zusammenhängt, hier mit den Pseudo-Terminals, habe ich vielleicht eine Art Limit erreicht? Nach einer kurzen Google-Recherche fand ich heraus, dass macOS ein Standardlimit für ptys hat, das auf 127 eingestellt ist.
$ sysctl kern.tty.ptmx_max
kern.tty.ptmx_max: 127
Wir scheinen also die Ursache gefunden zu haben! Aber was ist die Quelle? Welcher Prozess erzeugt so viele Pseudo-Terminals?
Wenn wir einen Test von Xcode aus ausführen, können wir mit lsof sehen, wie viele Terminals bereits belegt sind.
$ lsof /dev/ptmx
BEFEHL PID BENUTZER FD TYP GERÄTEGRÖSSE/OFF KNOTENNAME
iTerm2 1688 Benutzer 0u CHR 15,0 0t13596 572 /dev/ptmx
iTerm2 2164 Benutzer 0u CHR 15,1 0t71369123 572 /dev/ptmx
Xcode 2173 Benutzer 31u CHR 15,6 0t7887 572 /dev/ptmx
Xcode 2173 Benutzer 49u CHR 15,5 0t0 572 /dev/ptmx
Xcode 2173 Benutzer 54u CHR 15,8 0t0 572 /dev/ptmx
Xcode 2173 Benutzer 56u CHR 15,7 0t7 572 /dev/ptmx
Xcode erstellt etwa 5 Pseudo-Terminals und hält diese Anzahl konstant. Wenn wir einen Test von xcodebuild ausführen, können wir eine interessantere Ausgabe feststellen:
BEFEHL PID BENUTZER FD TYP GERÄTEGRÖSSE/OFF KNOTENNAME
iTerm2 1688 Benutzer 0u CHR 15,0 0t13596 572 /dev/ptmx
iTerm2 2164 Benutzer 0u CHR 15,1 0t71369123 572 /dev/ptmx
xcodebuil 8567 Benutzer 21u CHR 15,6 0t22726 572 /dev/ptmx
xcodebuil 8567 Benutzer 23u CHR 15,6 0t22726 572 /dev/ptmx
xcodebuil 8567 Benutzer 29u CHR 15,5 0t0 572 /dev/ptmx
xcodebuil 8567 Benutzer 31u CHR 15,5 0t0 572 /dev/ptmx
xcodebuil 8567 Benutzer 32u CHR 15,7 0t0 572 /dev/ptmx
xcodebuil 8567 Benutzer 34u CHR 15,7 0t0 572 /dev/ptmx
...
xcodebuil 8567 Benutzer 143u CHR 15,45 0t0 572 /dev/ptmx
xcodebuil 8567 Benutzer 145u CHR 15,45 0t0 572 /dev/ptmx
xcodebuil 8567 Benutzer 146u CHR 15,46 0t0 572 /dev/ptmx
xcodebuil 8567 Benutzer 148u CHR 15,46 0t0 572 /dev/ptmx
Jeder Test erstellt ein neues Terminal, das nicht freigegeben wird. Um Test 128 herum, wenn das letzte erlaubte pty gespawnt ist, kann Xcode keinen neuen Prozess auf dem Simulator starten und der Rest der Tests schlägt fehl.
Dieses Problem tritt nur im iOS-Simulator auf. iOS-Geräte sind von diesem Problem nicht betroffen.
Die erste Abhilfe
Offensichtlich hat der Apple-Entwickler vergessen, nach jedem Test etwas freizugeben und wir sind nicht in der Lage, dieses Problem ohne seine Hilfe zu beheben. Die einzige Möglichkeit ist also eine Art Workaround. Zum Glück können wir die maximale Anzahl der Pseudo-Terminals auf 999 erhöhen.
sudo sysctl -w kern.tty.ptmx_max=999
Wenn Sie sysctl verwenden, ist die Änderung nur vorübergehend (bis das System neu gestartet wird). Um den Wert dauerhaft zu speichern, erstellen oder öffnen Sie die Datei /etc/sysctl.conf mit Hilfe von:
sudo berühren Sie /etc/sysctl.conf
sudo chown root:wheel /etc/sysctl.conf
sudo chmod 644 /etc/sysctl.conf
echo "kern.tty.ptmx_max=999" | sudo tee -a /etc/sysctl.conf
Durch die Änderung von ptmx_max können wir jetzt bis zu 999 Testfälle in einem einzigen Schema testen, aber nur theoretisch...
App Zugänglichkeit
Wenn wir erneut mit dem Testen beginnen, finden wir ein weiteres interessantes Problem. Um Test 170 herum begrüßen uns Xcode oder xcodebuild mit dem folgenden Fehler:
Warten auf das Laden der Zugänglichkeit
Warten, bis die App im Leerlauf ist
App-Ereignisschleife Leerlaufbenachrichtigung nicht erhalten, versucht fortzufahren.
Die Benachrichtigung über den Abschluss der App-Animationen wurde nicht empfangen, es wird versucht, fortzufahren.
Nach diesem Fehler schlagen alle weiteren Tests mit einer anderen Meldung fehl:
Warten auf das Laden der Zugänglichkeit
Assertion Failure: testUITests.swift:21: UI Testing Failure - App Accessibility wird nicht geladen
Dieser Fehler ist noch interessanter als der vorherige. Er tritt nicht nur bei xcodebuild auf, sondern auch bei Xcode. Noch schlimmer ist, dass er sowohl im iOS-Simulator als auch auf dem iOS-Gerät auftritt. Leider hilft Google nicht weiter. Es gibt Themen im Apple Developer Forum, aber keine Lösungen.
Um dieses Problem zu untersuchen, habe ich die Haltepunkte "Test Failure" und "Exception" eingerichtet, um das Problem so schnell wie möglich zu erkennen. Der abgerufene Aufrufstapel enthält nichts Nützliches. Ich habe außerdem fs_usage verwendet, um den erstellten Prozess zu verfolgen und lsof, um zu überprüfen, wie viele Ressourcen vom Simulator und den Xcode-Tools verwendet wurden:
sudo fs_usage -f exec
sudo lsof -p
Leider ohne Ergebnis. "App accessibility" ist kein separater Prozess, es handelt sich also nicht um ein Problem, das mit dem Start einer App zusammenhängt. lsof hat etwa 700 offene Dateien angezeigt, während macOS ein Limit von 10240 offenen Dateien pro Prozess hat (sysctlkern.maxfilesperproc). Die Bash setzt dieses Limit auf 4864 offene Dateien (ulimit -S -n). Beide Werte liegen nicht einmal in der Nähe dessen, was Xcode zuweist.
Ich konnte weder eine Ursache noch eine Lösung finden.
Die zweite Abhilfe
Ohne eine gute Lösung besteht die einzige Möglichkeit darin, einen Workaround zu schaffen. Die einzige Lösung, die uns bei PGS Software eingefallen ist, besteht darin, mehr gemeinsame Schemata im Projekt zu erstellen. Ich würde vorschlagen, nicht mehr als 160 Testfälle in ein Schema aufzunehmen.
Update
Bei Xcode 8.3 beta sieht die Situation etwas besser aus. Die Tests schlagen nach 206 Testfällen fehl. In drei weiteren Jahren werden wir vielleicht 300 Tests durchführen können.
Update 14.06.2017
Unter Xcode 9.0 Beta ist es sogar noch besser. Die Tests schlagen nach 256 Testfällen fehl. Apple ist auf dem richtigen Weg, bald 300 zu erreichen.
Zusammenfassung
Bis Apple diese und wahrscheinlich noch weitere Probleme endlich behebt, wird das Schreiben und Ausführen von UI-Tests für alle iOS-Entwickler problematisch sein. Im Moment können wir nur nach Workarounds suchen und die Daumen drücken, dass Apple endlich ein funktionierendes und stabiles UI-Automatisierungstool entwickelt.
Links
Unsere Ideen
Weitere Blogs
Contact



