App-Technik, die begeistert! Über App-Architektur und Testing

Technische Einblicke am Beispiel von Scan & Go – Die mobile Einkaufslösung für die Thalia-App.

Einleitung

Wir, bei der Thalia Bücher GmbH, möchten unseren Kund*innen ein optimales Einkaufserlebnis anbieten. Aus diesem Grund stellen wir verschiedene Produktlösungen bereit, welche wir kontinuierlich verbessern.

Eines dieser Produkte ist die App „Thalia – Bücher entdecken“ für das Smartphone & Tablet (Android, iOS): https://www.thalia.de/vorteile/thalia-app.

Abb. 1: QR-Code zur App-Installation (Quelle: Thalia Bücher GmbH)

Um für unseren Kund*innen den Kauf zu vereinfachen, haben wir bei Thalia das Feature Scan & Go entwickelt. Ziel ist der kontaktlose, schnelle und mobile Einkauf in unseren Buchhandlungen mit der App. In Zeiten von Pandemie oder, um lange Schlangen an den Kassen in der Weihnachtszeit zu vermeiden, ein Mehrwert für unsere Kund*innen.

Allgemeine Informationen sind unter folgenden Webseiten zu finden: https://www.thalia.de/vorteile/scan-go oder https://www.youtube.com/watch?v=jCHEASuHVAc.

Alle Abbildungen stammen vom Autor, sofern keine Quelle angegeben ist.

Abb. 2: Scan & Go-Aufsteller mit QR-Startcode

So funktioniert Scan & Go

Nach der Installation bzw. nach dem Start der App kann in der Buchhandlung der QR-Startcode auf den hierfür bereitgestellten Aufsteller eingescannt werden (siehe Abbildung 2). Anschließend können Artikel anhand des Barcodes – z.B. auf der Buchrückseite – mit der App erfasst werden. Zum Schluss kann der Kauf innerhalb der App bequem getätigt und die Buchhandlung, ohne an der Kasse anstehen zu müssen, sorgenfrei verlassen werden.

Ziel

Mit diesem Tech-Blog-Artikel möchten wir aufzeigen, wie wir aus technischer Sicht Scan & Go entwickelt haben und welche Hürden wir zu bewältigen hatten. Des Weiteren wird ein Überblick über unser automatisches Testen von Scan & Go beschrieben. Der Artikel richtet sich an alle App-Entwickler*innen und technisch versierten Leser*innen.

Überblick

Die wesentlichen Bestandteile von Scan & Go werden mit Hilfe der folgenden Screenshots analog der User Journey dargestellt. Aufgrund des Umfangs werden nicht alle Screens vorgestellt (z.B. Hilfe oder Login).

Abb. 3: Übersicht von Scan & Go

Einstieg: Scan & Go kann über diverse Wege angesteuert werden: Über die Startseite der App, den internen Scanner in der App oder über die Kamera des Gerätes.

QR-Code-Scanner: Der Zugang zu Scan & Go wird durch das Einscannen von auf diversen Werbemitteln gedruckten QR-Codes in der Buchhandlung gewährt. Der Zugang ist zwei Stunden gültig.

Onboarding: Nach dem Einscannen des QR-Codes werden mit Hilfe eines optionalen, dreistufigen Onboardings die wichtigsten Funktionen und Informationen erklärt. Sie werden erst wieder angezeigt, nachdem der Zugang abgelaufen ist.

Artikel-Scanner: Das Herzstück: Im nachfolgenden Scanner werden die Artikel anhand des Barcodes erfasst. Im Bestätigungsdialog kann der Artikel begutachtet und auf Wunsch bearbeitet oder entfernt werden. Alle Artikel werden automatisch in der digitalen Einkaufstasche gespeichert.

Kauf & Bestätigung: In der Einkaufstasche werden alle Artikel in einer Liste zusammengefasst. Einzelne Artikel können geändert werden. Abschließend erfolgt nach der Bezahlung eine Bestätigung auf der Dankeseite. Der Einkauf ist damit erfolgreich abgeschlossen.

Implementierung

Für die Implementierung haben wir einen modernen Technologie-Stack gewählt, damit Scan & Go stabil läuft, einfach erweiterbar ist und sich über einen langen Zeitraum bewahren kann. Im Folgenden wird das technische Konzept erläutert und anhand von ausgewählten Beispielen vorgestellt. Scan & Go haben wir für iOS und Android entwickelt. Eingesetzte Technologien befinden sich im Anhang.

Architektur

Als übergreifendes Architekturkonzept haben wir die sog. “Clean Architecture” herangezogen (vgl. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html, Buchempfehlung: https://www.thalia.de/shop/home/artikeldetails/A1039840971). Ziel des Konzeptes ist, eine skalierbare, einheitliche und wartbare Implementierung anzustreben. Des Weiteren ist einer der wichtigsten Merkmale: Wir können verständlichen und gut testbaren Code entwickeln. Erst dadurch sind wir in der Lage, eine unerlässliche Testautomatisierung umzusetzen (siehe “Abschnitt Sicherstellung der Qualität & Automatisiertes Testen”).

Unsere Architektur ist in drei Schichten gegliedert: UI, Data und Domain (z.B. https://developer.android.com/topic/architecture).

UI: Die UI beinhaltet die Logik zur Präsentation von aufbereiteten Daten. Sie ist abhängig von Data und Domain und sollte keine Business-Logik beinhalten. Die UI ist eng an das Betriebssystem (z.B. Android) gekoppelt bzw. interagiert mit diesem.

Data: Beinhaltet die Business-Logik der App und implementiert die Datenhaltung und -beschaffung z.B. über REST-Services. Data ist lediglich von Domain abhängig.

Domain: Domain ist technisch das Bindeglied zwischen UI und Data. Ziel ist die Vermeidung von Redundanzen bzw. Bereitstellung von wiederverwendbaren Funktionalitäten (Use Cases) und weitaus komplexeren Business-Logiken. Dadurch wird die Interaktion zwischen UI und Data stark vereinfacht. Domain ist komplett unabhängig von UI und Data und kennt keine umgebende Infrastruktur (z.B. das Betriebssystem Android). Aus diesem Grund ist es sehr gut automatisch testbar. 

Für jeden Screen in Scan & Go wurde eine separate UI-Klasse implementiert und das übliche Entwurfsmuster „Model View Viewmodel“ verwendet. Der Unterbau für jede UI-Klasse setzt sich wie folgt zusammen (vereinfacht):

Abb. 4: Grober Aufbau einer UI-Klasse

  • Die UI-Klasse Screen arbeitet nur mit einem Viewmodel (durchgezogener Pfeil). Sie observiert das Viewmodel und präsentiert bei jeder Änderung eins zu eins die Daten.
  • Ein Viewmodel operiert mit Use Cases und verwendet lediglich die Daten aus dem Domain Model.
  • Ein Use Case greift ausschließlich auf abstrakte Repositories (Interfaces) zu und bereitet gemäß der gewünschten Business-Logik die Daten auf.
  • Data implementiert die Interfaces aus Domain (gestrichelte Linie), regelt den lokalen und externen Datenzugriff und bildet über Mapper-Klassen die entsprechende Datenstruktur ab. Dadurch hat beispielsweise eine Änderung von Attributen der Entity-Klasse in der Regel keinen Einfluss auf Domain und UI.

Beispiel: Implementierung des Artikel-Scanners (Android)

Nachfolgend werden die oben beschriebenen Zusammenhänge exemplarisch für den Artikel-Scanner-Screen mit Android-Code illustriert. Aufgrund der Komplexität und Schutz des Urheberrechts sind nicht alle Details abgebildet.

UI + Domain (Fragment, Viewmodel und Use Case): Für den Artikel-Scanner haben wir eine Klasse StorePaymentScannerFragment implementiert. Die Verdrahtung der Klassen erfolgt mittels Dependency Injection auf App-Ebene mit Dagger.

Abb. 5: Zusammenhang zwischen Fragment, Viewmodel und Use Case

  • (A) Mit Hilfe der Kamera des Gerätes kann der Barcode des Artikels erfasst werden. Die Erkennung des Barcodes erfolgt beispielsweise in Android mit dem ML Kit und mit CameraX von Google.
  • (B) Alternativ kann die EAN bzw. ISBN oberhalb des Barcodes manuell eingetragen werden.
  • (1) Nach Erkennung der EAN-Nummer wird diese an loadArticle übergeben.
  • (2) Der übergebene Code wird an den HandleScannedEanUseCase weitergereicht, welcher asynchron ausgeführt wird und die Beschaffung der Artikeldaten sowie die Speicherung kapselt.
  • (3) HandleScannedEanUseCase übergibt intern den Code an das Interface (siehe nächsten Abschnitt). 

Data (Repository, Mapper und API): In der nächsten Abbildung wird anhand einer eingescannten EAN-Nummer der passende Artikel aus dem Backend besorgt.

Abb. 6: Zusammenhang zwischen Use Case, Repository, Mapper und API
Abb. 7: Erfolgreich eingescannter Artikel

  • (1) Der HandleScannedEanUseCase greift auf das ihm bekannte Interface getArticle zu und übergibt u.a. die eingescannte EAN-Nummer. Implementierungsdetails sind dem Use Case nicht bekannt.
  • (2) Die Klasse StorePaymentArticleRepositoryImpl implementiert das Interface und greift auf die externe Datenquelle mit getStoreArticle zu. Die Implementierung unserer Schnittstelle StoreArticleAPI wird generisch mit Retrofit realisiert.
  • (3) Im letzten Schritt wird die Entität ArticleEntity von der API in die domänenspezifische Datenklasse StorePaymentArticle mit einem Subset notwendiger Daten abgebildet und an das Use Case zurückgeliefert. 

Final wird im Fragment der Artikel angezeigt. Es können weitere Artikel eingescannt werden. Dann werden erneut die drei beschrieben Schichten durchlaufen.

Sicherstellung der Qualität & Automatisiertes Testen

Um dauerhaft die Qualität, Stabilität und Zukunftssicherheit von Scan & Go zu gewährleisten, haben wir umfangreiche Unit- und UI-Tests implementiert. Die Tests sollen rechtzeitig Fehler aufdecken bzw. die fachlichen Anforderungen bewahren. Zudem wird ganz erheblich der manuelle Abnahmeprozess für ein neues App-Release reduziert. Somit sind wir in der Lage, schneller zu agieren und neue Features oder Bugfixes zu releasen.

Das Schreiben von Unit-Tests ist komplett in unserem Scrum-Prozess verankert. Es ist ein bedeutender Teil unserer Definition of Done (DoD). UI-Tests hingegen sind komplexerer Natur und werden in separaten Tickets entwickelt. Diese werden plattformübergreifend gemeinsam mit der QA spezifiziert und anschließend realisiert.

Unit-Tests

Aufgrund der gewählten Architektur und der strikten Trennung in einzelne Klassen ist es leicht, Unit-Tests zu schreiben. Der große Vorteil ist, dass diese Art von Tests sehr schnell direkt in einer JVM ausführbar sind. Ein Unit-Test ist deterministisch, wird isoliert ausgeführt und testet einen kleinen Bereich des Codes (eine „Unit“) ab. 

Während der Entwicklung werden bei jedem Commit einer Codeänderung auf den Entwicklungsbranch (vgl. Gitflow) automatisch alle Unit-Tests auf unserem Jenkins ausgeführt. Dadurch erhalten wir als Entwickler*innen schnelles Feedback, falls der Code Regressionsfehler enthält oder wichtige fachliche Anforderungen ausgehebelt wurden. Dementsprechend können wir rechtzeitig korrigierende Schritte einleiten.

Wir haben für Scan & Go alle Nicht-UI-Bereiche mit Unit-Tests abgedeckt. Im folgenden Codesnippet ist ein in Kotlin entwickelter Test exemplarisch abgebildet. Der Test prüft, ob falsch ausgezeichnete Barcodes (EAN, ISBN, …) identifiziert werden.

Abb. 8: Unit-Test zur Prüfung von invaliden EAN-Codes

UI-Tests

Technisch anspruchsvoller ist es, die Scan & Go-Screens mittels UI-Tests automatisiert zu testen. Grund ist nicht der zu schreibende Code, sondern vielmehr, dass die Tests auf physischen Geräten bzw. Emulatoren ausgeführt werden und eine deutlich längere Ausführungszeit brauchen. Zudem wird eine stabile Infrastruktur benötigt, wie z.B. Mockserver, Testmaschinen und ein dediziertes WLAN, um nur einige Punkte zu nennen.

Aufgrund der Infrastruktur und der externen Einflüsse und Abhängigkeiten können Tests scheitern, obwohl der Code nicht geändert wurde. Diese Tests sind sog. „flaky Tests“ und sollten vermieden werden. Für das Scheitern der UI-Tests sind folgende Einflussfaktoren maßgeblich:

  • Ausgelastetes Test-WLAN/Timeouts in den Service-Requests
  • Stabilität von REST-Services in der Testumgebung
  • Änderungen von REST-Services oder Webseiten von anderen Teams
  • App-Animationen (z.B. Einblendung von Bannern)
  • Dialoge (z.B. das BottomSheetDialogFragment von der Android-API)
  • (Ungeplante) Systemdialoge vom Betriebssystem
  • Zugriff auf die GPS-Position (z.B. Auffinden der nächsten Buchhandlung)

Wir sind die genannten Einflussfaktoren über einen längeren Zeitraum angegangen und haben für beide Plattformen Android und iOS inzwischen eine gute Erfolgsquote an positiven Tests (ca. 98 %, siehe Abbildung 9). Unser Ziel ist dennoch 100 %, um eine hohe Zuverlässigkeit mit den UI-Tests zu gewährleisten. Tests sollten ausschließlich scheitern, wenn der Code Regressionsfehler enthält oder wichtige fachliche Anforderungen ausgehebelt wurden.

Abb. 9: Testreport für unsere Kundenapp (Android)

Auf Basis dieser Vorarbeiten haben wir für jeden Scan & Go-Screen einen Test entwickelt und häufig genutzte Funktionalitäten getestet („happy path“). Aufgrund der langen Ausführungszeit haben wir auf Edge-Cases verzichtet (z.B. testen, ob ein Artikel 100 Mal in die Einkaufstasche gelegt werden kann).

Damit wir für beide Plattformen Android und iOS identische Testfälle haben, haben wir die zu testenden Schritte und deren Reihenfolge in gemeinsame Interfaces ausgelagert (siehe Abbildung 10). Ein Interface ist als Contract zwischen der Qualitätssicherung (QA) und der Entwicklung zu verstehen.

Abb. 10: Interface für einen gemeinsamen Testfall

Die jeweilige Plattform (Android, iOS) implementiert das Interface als normalen UI-Test (siehe Abbildung 11). Dabei müssen lediglich die atomaren Schritte implementiert werden. Die Reihenfolge der Schritte gibt das Interface in Form einer Methode vor (z.B. runTestScenarioAddKnownArticle). Diese Methode muss in der Testklasse für das entsprechende Testframework referenziert werden. 

Im folgenden Codesnippet ist eine Implementierung mit Kotlin exemplarisch abgebildet.

Abb. 11: Implementierung eines gemeinsamen Testfalls (Android)

Für Scan & Go haben wir 24 Tests konzipiert und in fachliche Bereiche gruppiert, z.B.:

  • Funktioniert der Einsprung nach Scan & Go?
  • Kann zur Hilfe navigiert werden und zurück?
  • Funktioniert der komplette Kaufprozess?
  • Wie verhält sich das Einscannen von bekannten/unbekannten Artikeln?
  • Funktioniert das Löschen eines Artikel ordnungsgemäß?

Insgesamt wurden in etwa 1200 Codezeilen implementiert. Die Tests werden täglich nachts ausgeführt und mit einem Report begutachtet:

Abb. 12: Auszug aus dem Testreport (Android)

Um das Schreiben von Tests für unsere QA einfach zu halten, haben wir uns entschieden, die Testinterfaces in Swift zu implementieren. Alle Testinterfaces werden als Library in einem Nexus-Repository deployed und in Android und iOS als übliche Dependency eingebunden. Für Android wird der Swift-Code automatisch im Buildprozess nach Kotlin konvertiert.

Weiterführende Informationen rund um unsere allgemeine Teststrategie haben wir in einem separaten Blog beschrieben: https://tech.thalia.de/testen-einer-app-in-der-hybriden-welt/.

Probleme & Lösungen während der Implementierung

Lichtverhältnisse

Abb. 13: Aktivierung des Lichts

Die Lichtverhältnisse in den verschiedenen Buchhandlungen variieren und sind aus technischer Sicht nicht immer optimal. Die Kamera des Gerätes hat unter Umständen Schwierigkeiten, den Barcode auf den Artikeln zu erkennen. Um dieses Problem zu lösen, bieten wir den User*innen eine einfache Möglichkeit, direkt beim Einscannen des Artikels das Licht des Gerätes zu aktivieren. Das technische Aktivieren des Lichts ist mit der Standard-API des jeweiligen Betriebssystems gelöst (z.B. androidx.camera.coreCameraControl).

Reduzierung der Serverlast

Um den User*innen ein bequemes und flüssiges Einscannen zu ermöglichen, werden während des Scanvorgangs die Aufnahmen der Kamera fortwährend automatisch analysiert und versucht, den Barcode des Artikels zu erfassen. Für jede erfolgreiche Erfassung eines Barcodes wird dieser an unser REST-Backend gesendet. Bei einer erfolgreichen Suchanfrage werden die Artikeldaten instantan geliefert und in der UI präsentiert. 

Die Optimierung besteht darin, nicht valide Barcodes zu erkennen und dem Backend vorzuenthalten. Das mindert nicht nur die Serverlast, sondern spart auch Datenvolumen der Kund*innen ein. Die Prüfung erfolgt direkt in der App und verifiziert den Barcode anhand der letzten Zahl im Barcode, der sog. Prüfziffer (vgl. https://de.wikipedia.org/wiki/European_Article_Number). 

Für Android haben wir den Algorithmus für die Prüfung beispielhaft in Kotlin wie folgt implementiert:

Abb. 14: Implementierung der Prüfung von Barcodes

Die App wertet das Ergebnis aus und sendet entsprechend die Ausgabe an das REST-Backend oder bricht ab und setzt den Scanvorgang fort.

WLAN

Sofern das Gerät nicht mit einem WLAN verbunden ist, kann eine App träge wirken und zu einer geringeren Zufriedenheit führen. Insbesondere ist die Netzabdeckung innerhalb der Buchhandlungen und der Shoppingcenter oft unzureichend. Aus diesem Grund bieten wir in den Buchhandlungen kostenlose WLAN-Zugänge an. Einmal mit dem WLAN verbunden, kann unsere App optimal performen, da die Artikeldaten signifikant schneller geladen werden können. Zudem wird kein mobiles Datenvolumen verbraucht.

Nach der Entwicklung der ersten Version für Scan & Go haben wir das Feedback erhalten, dass das Einscannen schneller sein könnte. Des Weiteren war den User*innen nicht bekannt, dass ein WLAN-Zugang in den Buchhandlungen vorhanden ist. Diese Probleme sind wir zügig angegangen. Ziel war in einer nächsten App-Version den Scan & Go-Vorgang flüssiger von der Hand gehen zu lassen und den User*innen eine verbesserte User Experience anzubieten.

Abb. 15: Einblendung Hinweisbanner für das WLAN

Wir haben in der App einen Hinweisbanner entwickelt, der angezeigt wird, wenn der QR-Code- oder der Artikel-Scanner geöffnet wird. Der Hinweisbanner wird nach ein paar Sekunden von oben eingeblendet. Er macht darauf aufmerksam, das WLAN in der Buchhandlung zu nutzen. Für den Absprung in die WLAN-Einstellungen wird die Standard-API des jeweiligen Betriebssystems verwendet.

Fazit & Ausblick

Aufgrund der gewählten technischen Architektur und der Teststrategie können wir mit dem Feature Scan & Go unseren Kund*innen einen Mehrwert anbieten, um schnell und intuitiv in einer Buchhandlung einkaufen zu können. Wir arbeiten weiter fokussiert an der Thematik und möchten aus Sicht des Kunden Scan & Go kontinuierlich erweitern und verbessern.

Langfristig wollen wir den Scan & Go-Prozess verschlanken und noch einfacher für unser Kund*innen gestalten. Beispielsweise möchten wir zukünftig das initiale Einscannen des QR-Startcodes obsolet machen. Damit wir wissen, in welcher Buchhandlung Scan & Go verwendet wird, müsste eine Ortungstechnologie herangezogen werden (WLAN-Ortung, Bluetooth Beacon, GPS, Geofencing, …). Beim Start von Scan & Go wird automatisch die Buchhandlung ermittelt und das Einscannen des QR-Startcodes entfällt. Das ist zudem nachhaltiger, da die Werbemittel zum Bedrucken des QR-Codes (Aufsteller, Flyer, …) eingespart werden können.

Weitere Ideen werden gesammelt und priorisiert umgesetzt. Im Entwicklungsprozess halten wir uns an Scrum und durchlaufen vereint alle Fachdisziplinen (UX, PO, QA, DEV), um ein stimmiges Endprodukt zu erreichen.

Anhang

Android iOS
Sprache Kotlin, teilweise Java (legacy Code) Swift, SwiftUI, teilweise Objective C (legacy Code)
OS-Version >= Android 8 >= iOS 14
IDE Android Studio Xcode
CI/CD Jenkins, git, GitLab, gradle, Bash, Pipeline-Scripting Jenkins, git, GitLab, xcodebuild, Bash, Fastlane
Netzwerk, Datenbank Retrofit, Room Alamofire, CoreData
QR-Code- und Text-Erkennung Google ML Kit, CameraX AVFoundation
Unterstützte Scan-Codes FORMAT_EAN_13, FORMAT_EAN_8
FORMAT_UPC_E, FORMAT_QR_CODE 
FORMAT_CODE_128 
EAN13Code, EAN8Code, UPCECode, QRCode, Code128Code
Test Espresso, Roboletric, Mockito, JUnit
Barista
XCTestCase, XCTest
Animation Lottie Lottie



Neues Swift-Feature Async/Await

Auf der diesjährigen Swift Heroes Konferenz (April 2021) wurde in einem Vortrag von Vincent Pradeilles das neue Swift Feature Async/Await (ab Swift 5.5) vorgestellt. Wir iOS Entwickler aus dem Thalia App Team haben uns den Vortrag angehört und waren begeistert von den neuen Möglichkeiten des Sprachfeatures, zur Vereinfachung von asynchronem Code. Andere Programmiersprachen (wie zB. C#) besitzen dieses Feature schon länger und wir sind froh, dass Swift jetzt nachgezogen hat.

Viele iOS Entwickler kennen das Problem, dass asynchroner Code schnell unleserlich, unübersichtlich und kompliziert werden kann. Genau dieses Problem soll Async/Await lösen, weshalb wir die Funktionsweise und Benutzung im folgenden Artikel mit Codebeispielen veranschaulichen wollen. Dabei stellen wir Async/Await auch den Alternativen gegenüber, um die Vorteile und Unterschiede zu bisherigen Verfahren deutlich zu machen.

Ziel des Artikels soll es sein, für Entwickler, die noch keine Berührung mit dem neuen Swift-Sprachfeature Async/Await hatten, die Grundlagen des Features zu erläutern. Für diejenigen, die sich schon ein bisschen mit dem Thema beschäftigt haben, empfehlen wir zur Vertiefung des Wissens die WWDC 2021 Session über Async/Await anzuschauen.

Problemstellung und Lösungsmöglichkeit mit Completions

In dem Beispiel aus dem Vortrag von Vincent Pradeilles soll der Anwender mit Vor- und Nachnamen begrüßt werden. (Konsolenausgabe) Dafür muss eine UserId geladen werden, um damit den Vor- und Nachnamen zu laden. Die klassische Lösungsmöglichkeit für dieses Problem in Swift sind Completions.

In Abb. 1 sehen wir, wie eine solche Problemlösung mit Completions aussehen könnte. Wir sehen das der Code nicht so gut leserlich ist, da er eine Verschachtelung von Methodenaufrufen enthält (Pyramid of Doom), was bei diesem Beispiel noch einigermaßen verständlich ist, aber bei mehr Aufrufen schnell sehr unleserlich und kompliziert werden kann.

Abb. 1: Asynchroner Swift Code mit Completions

Implementierung mit Async/Await

Abb. 2: Asynchroner Swift Code mit Async/Await

In Abb. 2 sehen wir, wie das Problem mit Async/Await gelöst werden kann. Das async-Keyword dient dazu dem Compiler zu signalisieren, dass in dieser Methode asynchrone Aufrufe stattfinden. Das await-Keyword bedeutet, dass hier auf das Ergebnis eines asynchronen Aufrufes gewartet werden muss. Im ersten Schritt wird über die Methode getUserId() die userId geladen. Erst wenn die userId geladen wurde, wird über die Methode getUserFirstname der firstname geladen. Im letzten Schritt wird der lastname geladen. D.h. die Aufrufe erfolgen seriell nacheinander.

Achtung: Enthält eine Methode ein await-Keyword, muss diese auch als asynchron über das async-Keyword markiert werden.

In Abb. 3 ist ein Beispiel zu sehen, um innerhalb der asynchronen Methode getUserId() eine Methode mit einer Completion aufzurufen (zB. um abwärtskompatibel zu bestehendem Code zu sein). Dazu kann man die Methode withCheckedContinuation aus der Swift Standard Library verwenden. Dabei wird die Rückgabe der UserId erst ausgeführt, wenn die resume Methode auf dem Continuation-Objekt aufgerufen wird.

Abb. 3: Aufruf einer Methode mit Completion aus einer mit async markierten Methode

In vielen Fällen ist es möglich asynchrone Methoden parallel auszuführen, um Zeit zu sparen. Mit Async/Await ist dies sehr einfach umzusetzen, durch Verwendung der Keywords async let wie in Abb. 4 zu sehen:

Abb. 4: Parallele asynchrone Methodenaufrufe mit Async/Await

Zunächst wird, wie im ursprünglichen Code, die userId geladen. Durch die Deklaration der beiden Variablen firstname und lastname mit async let werden die beiden Methoden getUserFirstname  und getUserLastname  parallel ausgeführt. Erst wenn beide Methoden einen Rückgabewert liefern, wird das print-Statement ausgeführt. Dies wird erreicht durch das await-Keyword vor dem print-Statement.

Lösung mit Combine

Eine andere Lösungsmöglichkeit in Swift ist Combine zu nutzen.

In Abb. 5 sehen wir, wie das Problem mit Combine gelöst werden kann.

Abb. 5: Asynchroner Swift Code mit Combine

Die Lösung mit Combine ist nicht so tief verschachtelt wie die klassiche Lösung mit Completions, allerdings für diesen Anwendungsfall auch nicht sehr leserlich und etwas kompliziert.

Was ist nun die bessere Lösung?

Die Frage lässt sich nicht so leicht beantworten. Combine ist ein Werkzeug, das viel mehr bietet als Async/Await. Allerdings löst Async/Await dieses spezielles Problem besonders gut. Je nach Anwendungsfall muss entschieden werden, ob eine Lösung mit Combine oder Async/Await die bessere Wahl darstellt.

Lösung mit besseren Server APIs 😉

Fazit

Wir haben gesehen, wie man mit Async/Await Code schreiben kann, der klassichen Code mit Completions ersetzt. Außerdem wie man Code mit Completions aus einer async-Methode aufrufen kann (Abwärtskompatibilität) und wie man mit async let mehrere Aufrufe parallel ausführen kann. Es wurde auch gezeigt das das Problem mit Combine lösbar ist.

Uns hat der Vortrag von Vincent Pradeilles auf der Swift Heroes 2021 und die WWDC 2021 Sessions zu Async/Await so gut gefallen, dass wir das Sprachfeature gerne in Zukunft in der Thalia App verwenden möchten. Dabei ist zu beachten, dass die Verwendung erst ab iOS 15 möglich ist.

Natürlich sind die anderen Lösungswege über Combine und Completions genauso valide und es muss von Fall zu Fall unterschieden werden, welche Lösung die Beste für das jeweilige Problem darstellt.




Testen einer App in der hybriden Welt

In der eCommerce-Welt von Thalia gibt es neben cross-funktionalen Teams, welche den Webshop u. a. unter thalia.de in bestimmten Domänen (z. B. Aftersales) betreuen und weiterentwickeln, auch sogenannte Touchpoint-Teams. Diese zeichnen sich dadurch aus, dass sie einen kompletten Touchpoint zum Kunden betreuen und weiterentwickeln. In unserem Fall handelt es sich um die Thalia App.

Während das App-Team als ein „geschlossener“ Touchpoint zu den Kund*innen hin auftritt, haben wir mit allen cross-funktionalen Teams innerhalb der Organisation Schnittstellen. Diese lassen sich grob in API-Aufrufe und Webviews, d. h. die Anzeige von Webseiten innerhalb der App, aufteilen. Das führt zu verschiedenen Herausforderungen in Bezug auf die übergreifende Qualitätssicherung:

  • Verantwortlichkeit: Wenn jedes Team seinen eigenen Code testet, wer (und wie) testet (man) dann über Teamgrenzen hinweg?
  • Redundanz: In welchem Umfang ist Redundanz akzeptabel, erwünscht, notwendig?
Nimm du ihn, ich habe ihn sicher!

Der Webshop ist ein lebendes Objekt. In den letzten Jahren wurde vieles getan, um den alten Monolithen durch eine Self-Contained-Systems-Architektur abzulösen. Insgesamt arbeiten derzeit sechs cross-funktionale Teams mit verschiedenen Schwerpunkten an ihren jeweiligen (oder übergreifenden) Themen, lösen die allerletzten Reste aus dem Monolithen heraus und entwickeln Features weiter oder neu, um das Kund*innenerlebnis besser zu machen. Mit gängigen CI/CD-Praktiken werden mehrmals täglich Änderungen produktiv genommen bzw. veröffentlicht. Diese können sich auch auf die App auswirken, wenn sie beispielsweise die eingebundenen Webviews betreffen. Auch im nativen Teil tut sich einiges: Wir entwickeln die App weiter und veröffentlichen in regelmäßigen Abständen neue Versionen.

Bei so viel Veränderung muss automatisiert – also durch automatisierte Tests – sichergestellt werden, dass die einzelnen Teile in sich funktionieren, aber auch ineinandergreifen.

Einfach: Testen, was man selbst entwickelt

Stark vereinfacht kann man sagen, dass der native Teil der App ein (vom Webshop) separates Frontend darstellt, welches auf REST-Schnittstellen der cross-funktionalen Teams aufsetzt. Um die Funktionalität dieses Frontends zu testen, verwenden wir zum einen Unit-Tests und zum anderen UI-Tests. Werden Änderungen im Code vorgenommen, führen wir diese beiden Arten von Tests aus. Bei den UI-Tests werden die Backend-Daten aus den REST-Schnittstellen gemockt, ähnlich wie z. B. bei WireMock. So nimmt man die potenzielle Fehlerquelle Backend aus der Gleichung und hat zusätzlich die Möglichkeit, Fehlerzustände durch HTTP-Fehlercodes zu simulieren.

Auf diese Weise stellen wir sicher, dass wir – das App-Team – mit unseren Änderungen nichts kaputt machen bzw. die Qualität beibehalten und wir bei Fehlern frühestmöglich reagieren können.

Fortgeschritten: Die REST-Schnittstellen anstöpseln aka den nativen Teil integrativ testen

Um zu prüfen, ob das Zusammenspiel zwischen den REST-Schnittstellen und dem nativen App-Code so funktioniert, wie wir es vom Mockserver simulieren lassen, führen wir unsere UI-Tests auch gegen die tatsächlichen Schnittstellen aus. Diese Ausführung findet nächtlich statt und ist leider nicht ganz so stabil wie die Variante mit Mockserver.

Über die Zeit haben wir verschiedene potenzielle Fallstricke identifiziert, die diese Ausführungen fehlschlagen lassen, obwohl eigentlich nichts kaputt ist (ja, sicher…). Dazu gehören z. B. Timeouts durch Probleme mit dem Netzwerk oder sich ändernde Stammdaten im Testsystem. Durch Verbesserungen wie ein separates WLAN, in dem nur die Testgeräte hängen oder interne „Testdatentools“, über welche Stammdaten mit bestimmten Eigenschaften angefragt werden können, versuchen wir diese Risiken zu minimieren. Siehe auch Bereitstellung von Testdaten einmal smart.

Sollte ein Test fehlschlagen, welcher mit dem Mockserver erfolgreich ist, ist eine tiefergreifende Analyse nötig. Ergebnis kann sein, dass der Test aus irgendeinem Grund „flaky“ (unzuverlässig) ist oder der Aufruf einer REST-Schnittstelle zu einem Fehler führt. Für die Ursachenforschung und -behebung wird in einem solchen Fall häufig das Team eingebunden, welches die REST-Schnittstelle zur Verfügung stellt.

Die Königsdisziplin: Webviews testen

Richtig spannend wird es dann, wenn wir den nativen Teil der App verlassen. Die gegen den Mockserver ausgeführten Tests werden hier abgebrochen, da die Funktionalität der Webview kaum zu simulieren ist. Eine Webview ist aus Sicht der App gewissermaßen eine Blackbox. Wir können zwar mit ihr interagieren, haben aber keinen Einfluss auf ihr Verhalten. Auch bekommt die App Änderungen in einer Webview nicht ohne Weiteres mit, d. h. der Weg aus der Webview zurück in die App ist eine zusätzliche Schwachstelle. Erschwerend kommt hinzu, dass die Webviews, wie die REST-Schnittstellen, von anderen Teams entwickelt und betreut werden.

Austausch von Daten zwischen dem nativen Teil und der Webview

Wechseln wir also vom nativen Teil der App in eine Webview, übergibt die App gewissermaßen das Steuer – mittels automatisierten Tests stellen wir sicher, dass hierbei keine Fehler passieren.

Daten werden an Webview übergeben

So wird zum Beispiel der OAuth-Token zur Kund*innen-Authentifizierung oder eine Liste von Artikeln beim Aufruf des Warenkorbs an die Webview übergeben. Mittels UI-Test überprüfen wir dann, ob beim Wechsel in den Warenkorb die zuvor hineingelegten Artikel angezeigt werden. Die Darstellung dieser Artikel sowie die verschiedenen Interaktionen auf der Warenkorb-Webview selbst wiederum werden vom bereitstellenden cross-funktionalen Team getestet.

Um durch die Webview zu navigieren ist es wichtig, dass es eine teamübergreifende Absprache darüber gibt, wie bestimmte (HTML-)Elemente identifiziert werden. Die Vergangenheit hat gezeigt, dass sich das Label eines Buttons häufiger ändert, beispielsweise von „weiter“ auf „zur Kasse“, es aber immer einen Button mit gleichbleibender Funktionalität gibt. Deshalb verwenden wir eindeutige Selektoren wie „interaction“ oder das eigens für das Testen angelegte „data-test“-Attribut. Teilweise müssen wir auch auf „id“ zurückgreifen, Stichwort „lebendes Objekt“. Auch haben sich einige Drittanbieter noch nicht bereit erklärt, durchgängig den Thalia-Standard zu verwenden. Schäme dich, großer, sehr bekannter Zahlungsdienstleister! Zum Vorteil von separaten HTML-Attributen: Robust locator strategy: custom attributes for test automation (Medium).

Über diese Selektoren hangelt sich der UI-Test über die Webview, füllt ein Kontaktformular aus oder klickt auf Buttons. Von all dem bekommt der native Teil der App allerdings nichts mit, d. h. alle vom UI-Test simulierten Kund*innen-Interaktionen spielen sich in besagter Blackbox ab.

Ist der*die Kund*in unangemeldet auf den Warenkorb navigiert und hat sich auf dem Weg in die Bestellübersicht angemeldet? Diese Information erhält der native Teil der App nur dann, wenn die Webview ihn darüber informiert. Dies geschieht über sogenannte Callbacks. Sie sind der einzige Weg aus einer Webview zurück in den nativen Teil der App, wenn nicht der gesamte Vorgang abgebrochen und die Webview selbst verlassen wird. Weiterführender Artikel zu diesem Thema:  WebView- Love it or hate it, but you can’t ignore it (Medium).

Teamwork beim Scannen von Geschenkkarten

Ein anderes Beispiel für einen Callback ist der Geschenkkarten-Scanner. Dieser ist Teil der Bestellübersicht und bietet Kund*innen die Möglichkeit, den Barcode auf einer Thalia-Geschenkkarte einzuscannen, anstatt die vielen Zeichen selbst einzugeben.

Bei dem Scanner handelt es sich um einen nativen Teil der App, welcher mittels Callback geöffnet wird, und der – wiederum mit einem Callback – die erkannte Zeichenfolge an den Webview übergibt. Hier gibt es also gleich mehrere mögliche Fehlerquellen:

  1. Webview: Wird der Callback zum Öffnen des nativen Scanners (korrekt) gefeuert?
  2. App: „Versteht“ die App den Callback, also weiß sie, dass der Geschenkkarten-Scanner geöffnet werden muss?
  3. App: Kann der Barcode vom nativen Scanner eingelesen werden?
  4. App: Wird der Callback zur Rückgabe der erkannten Zeichenfolge (korrekt) gefeuert?
  5. Webview: „Versteht“ die Webview den Callback und schreibt die Zeichenfolge in das richtige Feld?

Während die Verantwortlichkeit für die Punkte 1 und 5 bei dem cross-funktionalen Team liegt, welches die Bestellübersicht betreut, liegt sie für Punkte 2 bis 4 bei der App.

Hier ist schön zu sehen, was ein UI-Test in der App alles implizit leistet. Denn auch wenn die Fragen alle isoliert (über bspw. Unit-Tests) getestet und mit „Ja“ beantwortet werden können, haben wir erst mit dem integrativen UI-Test eine Aussage darüber, ob das Zusammenspiel funktioniert. Nur ein in der App ausgeführter Test kann Sicherheit bringen und die fünf Prozessschritte zuverlässig zusammenstöpseln – ob automatisiert durch einen UI-Test oder manuell ausgeführt auf einem der zahlreichen mobilen Testgeräte, die über alle Teams verteilt sind. Dass jedes Team Zugriff auf ein Testgerät hat, ist vor allem wichtig und wertvoll, wenn an Komponenten gearbeitet wird. Die korrekte Funktionsweise, auch in der App, muss von allen Beteiligten kurzfristig sicherzustellen sein – Stichwort CI/CD der cross-funktionalen Teams.

Zusammenspiel der Webviews verschiedener Teams

Neben den Übergängen zwischen dem nativen Teil der App und den Webviews gibt es auch innerhalb der Webviews Kontextwechsel, welche teilweise bedeuten, dass die Domäne eines cross-funktionalen Teams verlassen und die eines anderen betreten wird. Navigieren nicht angemeldete Kund*innen z. B. vom Warenkorb auf die Bestellübersicht (beides Team A), wird zunächst auf die Anmelde-Seite weitergeleitet (Team B), um sich zu authentifizieren.

Erst danach kann er*sie mit der Wahl seiner*ihrer Versandoption und Zahlungsart in der Bestellübersicht fortfahren (zurück zu Team A). Hier gilt ähnliches wie beim Beispiel des Geschenkkarten-Scanners: Der durch die App navigierende UI-Test überprüft, ob alles wie gewünscht ineinandergreift.

Da diese Übergänge aber nicht nur in der App, sondern auch im Webshop existieren, gibt es ein weiteres, noch größeres Sicherheitsnetz, welches vom Team „zentrale QA“ über mehrere Browser für die Webvariante gespannt wird. Einen Eindruck über die Arbeit der zentralen QA gibt der Artikel True Grid oder wie wir unsere Testausführung durch Parallelisierung 6x schneller machten.

Die Herausforderungen des (App-)Testing bei Thalia

Das Testen in der App bringt viele Herausforderungen mit sich. Vor allem Abhängigkeiten zu anderen Teams und das Testen der unterschiedlichen integrativen Szenarien wie Verwendung von REST-Schnittstellen sowie die Einbindung der Webviews sind nicht nur technologisch, sondern auch organisatorisch anspruchsvoll. Deshalb schätze ich die Teamstruktur und -kultur bei Thalia sehr, bei der ich mit allen Kolleg*innen über Teststrategie oder ganz konkrete Probleme/Fehlerfälle sprechen kann. Denn für jedes Feature, das überarbeitet oder neu entwickelt wird, muss mit allen Beteiligten auch immer ein angemessenes Maß an Tests gefunden werden.

Um zu den Fragen vom Anfang zurückzukommen: Was liegt in wessen Verantwortung, wo ist Redundanz notwendig, überflüssig, wünschenswert? Da das App-Testing so aufwendig und kompliziert ist, kann man nur sagen: Es gibt keine allgemeingültige Formel, kein Schema F, nur die Erfahrungen, die wir inzwischen gesammelt haben und an einigen Stellen erneut anwenden können.




Continous Integration in der App-Entwicklung

Am Standort Berlin entwickeln wir für unseren B2B-Partner Douglas unter anderem die Kunden-App [2], [3]. Einhergehend mit dem Ausbau der App-Entwicklungsaktivitäten haben wir in den letzten Monaten den CI-Ansatz überarbeitet. Ein zusätzliches Team sollte am selben Produkt – der KundenApp – mitarbeiten und die App sollte öfter veröffentlicht werden. Mit Methoden und Tools aus dem Bereich Continous Integration wollten wir dafür sorgen weiter zuverlässig und mit hoher Qualität zu liefern. Und das natürlich automatisiert. Neben der Technik geht es auch um die Teams. Wie sind sie vorgegangen und welche Hürden haben sie genommen.

Komplexität und Feedback

Je mehr Personen gleichzeitig an einem Produkt entwickeln, desto größer wird die Wahrscheinlichkeit, dass unbeabsichtigte Seiteneffekte auftreten. Gleichzeitig steigt der Umfang der App, da wir konstant neue Funktionen hinzufügen und bestehende Funktion ändern. Um die Komplexität weiter zu beherrschen, ist schnelles Feedback zu Änderungen ein entscheidender Faktor, um Probleme schnell zu korrigieren. Wie wäre es, automatisiert ein Feedback nach jedem Commit zu bekommen und darauf nur kurze Zeit warten zu müssen? Genau hier setzten wir an.

Schnelles Feedback erhalten wir durch den Einsatz von Feature-Toggles und durch die Ausführung von automatischen Tests im CI-Prozess. 

Feature Toogles 

Feature Toogles ermöglichen es uns Codeänderungen aller Entwickler kontinuierlich in einen gemeinsamen Integration-Branch zusammenzuführen. Und das auch, wenn Features noch nicht fertig bzw. für den Kunden nicht sichtbar sein sollen. In der Vergangenheit haben wir solche Features für mehrere Tage, manchmal Wochen, in separaten Branches entwickelt und erst am Ende der Entwicklung in den Integration-Branch gemergt. Das Feedback kam entsprechend spät. Traten Probleme auf, war es durch die Vielzahl der Änderungen mitunter schwer festzustellen, welche konkrete Änderung zum Bruch geführt hat. Die Integration hatte das Potential unsere Zeitplanung empfindlich zu stören. Diese Bing-Bang-Szenarien sollen durch Toggles und kontinuierliche Integration abgefedert werden. 

Toggles und Diskussionen

Der Einsatz der Feature-Toggles wurde im Team intensiv diskutiert, denn die Einführung erhöht erstmal die Komplexität – und liebgewonnen Pfade, wie die isolierte Arbeit im Feature-Branch, standen auf einmal auf dem Prüfstand. Es gab diverse Pros und Cons. Auch musste ein gemeinsames Verständnis beim Thema Toggle-Mechanik erarbeitet werden. In Bezug auf Dynamik und Langlebigkeit der Toggles gab es unterschiedliche Auffassungen, da viele schon mal irgendetwas mit Toggels gemacht hatten. 

Wir haben uns am Ende auf die Nutzung von Feature-Toggels zur Entwicklungszeit – auch Release-Toggles genannt – geeinigt. Sie werden für den Zeitraum weniger Tage/Wochen genutzt. Ist das Feature fertig entwickelt, wird der Toggle aus dem Code komplett entfernt. Der Artikel auf martinfowler.com [1] sei dem interessierten Leser an der Stelle empfohlen. 

https://martinfowler.com/articles/feature-toggles.html

2 bis 3 Feature-Toggle sind im Durchschnitt parallel im Einsatz. In unserem Jenkins haben wir durch einen manuellen Schritt in der Build-Pipeline die Möglichkeit geschaffen, einen einzelnen Toggle zu aktivieren und somit App-Artefakte für Features (apk, ipa) für das Testing zu bauen. Ist das Feature komplett entwickelt, wird der Toggle aus dem Code entfernt. Mit dem nächsten App-Release ist das Feature dann für den Kunden sichtbar.

Was ist ein Feature Toggle?

Ein Feature Toggle ist eine Progammiertechnik, die es erlaubt ein Feature oder eine Funktion vor Kunde ein- bzw. auszuschalten. Also die Sichtbarkeit zu ändern. Entwicklungsteams aktivieren Features beispielsweise um noch nicht fertige Funktionen integrieren und testen können. Ist ein Feature fertig, kann es ohne großen zusätzlichen Merge-Aufwand veröffentlicht werden, da die Arbeit in separaten Branches entfällt. Feature Toggles können auch dafür genutzt werden, die Sichtbarkeit von Funktionen zur Laufzeit der Anwendung zu ändern. Z.B. im Rahmen von A/B Tests oder wenn die Sichtbarkeit zu einer bestimmten Zeit geändert werden soll.

Toogles im Code

iOS

Unter iOS wird ein Feature in der App über eine Environment-Variable in der Launch-Konfiguration aktiviert (z.B.: USE_NATIVE_PRODUCT_LIST = 1). Im Code wird dann an relevanten Stellen über eine Abfrage entschieden, ob bestimmte Codestellen zur Ausführung kommen oder nicht.

if toggleRouter.isNativeProductListEnabled() {
  // Feature Code
}

Android

Es gibt ein Interface, in dem alle Toggles als Methoden definiert werden. Diese Methoden werden mit Java-Annotations annotiert und geben immer ein Boolean zurück – TRUE für Feature aktiv, FALSE für nicht aktiv.

@ReleaseToggle(BuildConfig.FEATURE_PRODUCT_LIST)
fun isProductListEnabled(): Boolean

Eine eigens dafür entwickelte Library mit einem Annotation-Prozessor wird während der Build-Phase ausgeführt: Dieser schaut in einer Konfigurations-Datei (json) nach, ob das jeweilige Feature getoggelt werden soll. Wenn das Feature eingeschaltet werden soll, muss der String, der sich in der Annotation befindet, hier eingetragen werden.

[
    "FEATURE_PRODUCT_LIST"
]

Der Prozessor baut dann jeweils die Implementation für das Interface zusammen. In diesem Fall würde die implementierte Methode TRUE zurück liefern. Wäre der String FEATURE_PRODUCT_LIST nicht in der Datei, wäre es FALSE.

So kann man auf jedem lokalen Rechner die Features beliebig ein- und ausschalten. Auf dem Jenkins kann man das genauso machen, hier editieren wir nicht manuell die Datei sondern sagen ihm über das Blue Ocean Plugin, welches Feature getoggelt werden soll.

Und die jeweiligen Code-Stellen togglen wir, in dem wir die Interface-Implementation prüfen:

if (ReleaseToggleConfiguration_Impl().isProductListEnabled()) {
     // Mach was mit der Product list
}

Ein gemeinsames Traffic Light für Build und Test

Eine weitere zentrale Komponente im CI-Prozess stellt die Testautomatisierung dar. Das Feedback, dass Build und Test erfolgreich nach einem Commit auf Integration durchgelaufen sind, wird durch eine Ampel visualisiert. Diese ist für jedem im Team sichtbar. Ist sie rot, ist das gesamte Team angehalten den Grund zu ermitteln und die Ampel wieder auf „grün zu bekommen“. Also Fehler zu korrigieren, Tests oder die Automatisierung anzupassen.

CI-Build-Status für Android und iOS

Die Tests sind eine Kombination aus Unit-Tests und End-2-End-Tests (Akzeptanztests). Die End-2-End-Tests laufen auf echten Geräten bzw. Simulatoren im Zusammenspiel mit dem Backend.

Continous Integration Process

Unser CI Prozess sieht wie folgt aus:

Nach dem Approval der Codeänderung in Gitlab und Integration in den Integrations-Branch baut der Jenkins das App-Artefakt, führt die Unittests aus und startet die End-2-End Tests. Das kombinierte Ergebnis aus Build/Unitests und End-2-End Test wird auf der Ampel dargestellt.

Für den Test eines Features, dass sich noch in der Entwicklung befindet, wird ein Feature-Toggle manuell im Jenkins aktiviert, die App gebaut und die Unittests ausgeführt. Die End-2-End Tests werden zum jetzigen Zeitpunkt noch nicht ausgeführt. Zum einen müssten die Tests für das Feature bereits angepasst und erweitert sein. Das ist noch nicht der Fall. Ein weiterer Grund sind die noch fehlenden Ressourcen in Form von Hardware und Testgeräten. Ein nächster Schritt.

Learnings zu Tools, Integrationslevel und Verantwortung

Auf drei Learnings möchte ich an der Stelle speziell eingehen. 

Der Leser soll dazu wissen, dass unsere Entwicklungsteams Cross-funktional aufgebaut sind. Ein Entwicklungs-Team besteht aus iOS-Entwicklern, Android-Entwicklern, Backend-Entwicklern und einem Quality Engineer. Die Backend-Entwickler sitzen an einem anderen Standort und sind wie wir Dienstleister für den B2B-Partner. Folgende Tests führen wir zur Zeit durch:

Testtypen

Beim Tooling ging es vor allem um die Wahl des Testautomatisierungstools. Da die QA in der Vergangenheit auf Appium gesetzt hat, wollten wir die Technik auch für unsere CI-Tests im gesamtem Team nutzen (Dev+QA). Bisher wurden die Appium Test ausschließlich von der QA geschrieben und waren nicht in einem gemeinsamen CI-Prozess zusammen mit dem Build integriert. Es stellte sich heraus das die Akzeptanz des Tools für iOS unter den Entwicklern sehr gering war. Stabilität, Funktionsumfang, Integrierbarkeit und Ausführungsgeschwindigkeit überzeugten nicht. Unsere Teams haben sich daher entschieden, für iOS die End-2-End Tests auf Basis von XCUITest neu zu schreiben. Für den Android Bereich setzen wir vorerst weiter auf Appium.

Ein weiteres Learning gab es beim Integrationslevel. Unsere End-2-End Tests weisen ein hohes Integrationslevel auf: die App wird im Zusammenspiel mit dem Backend getestet. Fehler im Backend oder eine schlechte Verbindungsqualität können dazu führen, dass Tests fehlschlagen, obwohl die App „korrekt“ funktioniert. Die Ampel zeigt rot, obwohl „mit der App alles in Ordnung ist“. Flaky Tests bzw. instabile Tests senken die Aussagekraft der Ampel deutlich und führen dazu, dass die Teams einer roten Ampel weniger Aufmerksamkeit schenken. Neben dem End-2-End Test planen wir daher einen zusätzlichen Testtyp einzuführen, der vom Integrationslevel zwischen Unittest und End-2-End Test liegt. Ziel ist es die App ohne Backend zusätzlich zu verifizieren. Dafür sollen Backend-Responses „gemockt“ und die Tests auf der UI-Ebene durchgeführt werden. Die Test sollen eine Ergänzung zu den End-2-End Tests werden.

Beim Thema Verantwortung gehen wir mit dem CI-Ansatz ebenfalls neue Wege. Das Ergebnis aus Build + Test in einem gemeinsamen Ampelstatus zu visualisieren und damit jeden im Entwicklungs-Team zu aktivieren, Probleme im Test oder Build zu analysieren, erfordert, dass sich Entwickler mehr mit dem Thema Testing und sich die QA mehr mit der Automatisierung auseinandersetzt. Dieser Veränderungsprozess benötigt Zeit und den Willen aller Beteiligten sich zu verändern. In unserem Produktteams ist diese Veränderung explizit gewünscht und alle im Team sind angehalten für den Prozess Verantwortung zu übernehmen und ihn aktiv weiterzuentwickeln.

Ausblick

Im Bereich Testing steht der Ausbau der Testautomatisierung für die End-2-End Tests und die Einführung der zusätzlichen Test-Verifikationsstufe für die Android-App mit Espresso als UI-Testing Tool an.

Um die Qualität zu steigern, möchten wir automatisiert statische Codeanalysen durchführen und Metriken wie beispielsweise die technische Schuld ermitteln.

Verweise

[1] https://martinfowler.com/articles/feature-toggles.html

[2] Douglas App iOS: https://itunes.apple.com/de/app/douglas-parfüm-kosmetik/id394685685?mt=8

[3] Douglas App Android: https://play.google.com/store/apps/details?id=com.douglas.main&hl=de