Fallstudie: CI/CD und Unit-Tests für drei White-Label-Banking-Apps in zehn Wochen
Wie Mobian Studio ein Fintech-SDK und drei White-Label-Banking-Apps von manuellen Builds auf CI/CD umstellte und die SDK-Unit-Tests fast verdreifachte.
Die meisten Artikel über CI/CD beschreiben, wie eine Pipeline aussehen sollte. Dieser beschreibt eine Pipeline, die wir tatsächlich gebaut haben: für eine Fintech-Plattform, deren Android-SDK drei White-Label-Banking-Apps antreibt.
Als wir anfingen, hatte keines der sechs Repositories ein funktionierendes CI. Builds wurden von Hand erstellt, Versionsnummern von Hand in Commits hochgezählt, und eine Bibliothek existierte nur auf dem Rechner eines einzigen Entwicklers. Zehn Wochen später teilten sich das SDK und drei Kunden-Apps ein CI/CD-System, jeder Tester-Build ließ sich aus einem Git-Tag reproduzieren, und die Unit-Test-Suite des SDK hatte sich fast verdreifacht.
In diesem Artikel gehen wir die Ausgangslage, das Pipeline-Design, die Teststrategie, die Ergebnisse mit Datum und die noch offenen Aufgaben durch.
Die Ausgangslage
Der Kunde ist eine Fintech-Plattform, die hinter mehreren Banking- und Payment-Apps steht. Zur Produktfamilie gehören:
-
Ein Android-SDK aus rund 29 Gradle-Modulen, das Backend-JSON in native Screens umwandelt und außerdem Zahlungen, Biometrie und Kartenscans übernimmt.
-
Drei White-Label-Banking-Apps auf Basis dieses SDK, jede mit eigenem Branching- und Release-Modell.
-
Eine Netzwerkbibliothek und eine End-to-End-Test-Suite, die der QA gehört.
Im Juli und August 2026 sah die Lage so aus:
-
Im SDK-Repository lagen alte CI-Vorlagen, aber nichts lief.
-
Versionscodes wurden von Hand hochgezählt: rund 95 solcher Commits in einer App seit 2025 und 28 in einer anderen.
-
Ein Build ließ sich aus einem sauberen Checkout nicht reproduzieren, weil eine Bibliothek nur im lokalen Maven-Cache eines Entwicklers lag.
-
Einige Testmodule des SDK ließen sich nicht einmal kompilieren – das zeigte gleich der allererste CI-Lauf.
-
Das SDK hatte 1.122 ausgeführte Tests und 8,1 % Gesamtabdeckung, die Haupt-App 56 Tests.
📌 Wichtige Erkenntnis:
Niemand im Team hat etwas falsch gemacht. Ohne Pipeline lebt das Release-Wissen einfach in den Köpfen und auf den Laptops der Leute – und im regulierten Bankenumfeld ist das ein Risiko für sich.
Die Rahmenbedingungen
Die Pipeline musste zur realen Infrastruktur des Kunden passen, nicht zu einer idealen:
-
Selbst gehostetes GitLab, im Juli 2026 von gitlab.com migriert.
-
Ein gemeinsam genutzter macOS-Runner ohne Docker, den auch andere Projekte des Unternehmens nutzen und dessen Workspace zwischen Jobs erhalten bleibt.
-
Builds über zwei Repositories hinweg: Jeder App-Build kombiniert einen App-Branch mit einem SDK-Branch, der möglicherweise noch nicht gemergt ist.
-
Ein reguliertes Bankenumfeld, das reproduzierbare Builds, eine Release-Signierung außerhalb von Git und Abhängigkeitsscans nahelegt.
Was wir gebaut haben: eine Pipeline für drei Apps
1. Ein Webformular bis zu den Testern
Ein Tester-Build startet über ein einziges GitLab-Formular: App-Branch, SDK-Branch, Build-Typ und Testergruppen auswählen. Die Pipeline durchläuft dann sechs Stufen:
-
Prepare – löst den SDK-Branch auf einen einzigen Commit auf, sodass jeder spätere Job genau denselben SDK-Code verwendet.
-
Test – Unit-Tests der App und des SDK, Coverage und ein Abhängigkeitsscan.
-
Build – eine signierte APK (plus das AAB für den Play Store bei Releases), mit Signing-Zugangsdaten aus CI-Variablen.
-
Distribute – Firebase App Distribution an die ausgewählten Testergruppen.
-
Notify – ein Jira-Kommentar mit Build-Link und Coverage an jedem enthaltenen Ticket und die Verschiebung nach „Ready for QA“.
-
Tag – derselbe Tag auf dem App-Commit und dem SDK-Commit.
2. Jeder Tester-Build ist reproduzierbar
Weil der Tag nach der Verteilung in beiden Repositories gesetzt wird, bedeutet ein Tag: „Genau dieser Build hat die Tester erreicht.“ Wer den Tag in beiden Repos auscheckt, reproduziert den Build. Vorher war das überhaupt nicht möglich.
3. Eine Vorlage, mechanische Portierungen
Alle drei Apps nutzen dieselbe sechsstufige Pipeline. Nur wenige Konstanten unterscheiden sich – Jira-Label, Tag-Präfix, Testergruppen – und sie liegen in einem einzigen versteckten Template-Job. Die zweite und dritte App waren in wenigen Tagen portiert, obwohl eine davon ein Release-Branch-Modell nutzt und sieben separate Blocker aus dem Weg geräumt werden mussten.
4. Qualitätsprüfungen dort, wo die Leute ohnehin arbeiten
-
Testergebnisse im Tests-Tab des Merge Requests.
-
Ein Badge für die Logikabdeckung im Merge-Request-Widget und Coverage-Markierungen an geänderten Zeilen.
-
SonarQube auf dem Main-Branch.
-
OWASP Dependency-Check mit einer CycloneDX-SBOM in der Haupt-App.
5. Ein gemeinsamer Runner gilt als nicht vertrauenswürdig
Secrets liegen ausschließlich in CI-Variablen, auf dem Runner wird nichts gespeichert, jeder Job klont das SDK in seinen eigenen Workspace (nachdem parallele Jobs einmal gegenseitig ihre Checkouts gelöscht hatten), und signierte Release-Builds umgehen grundsätzlich den Build-Cache.
Was wir gebaut haben: eine Teststrategie, die bei der Architektur beginnt
Die Testabdeckung war der Maßstab, doch die eigentliche Arbeit bestand darin, den Code testbar zu machen. Vier Entscheidungen haben das Ergebnis geprägt.
1. Den Code messen, auf den es ankommt
Wir haben ein Logik-Set definiert: Domain, Daten, Android-unabhängige Utilities, die Request- und Event-Engines sowie die Basis-ViewModels. UI-Code, DI-Verdrahtung und generierter Code werden weiterhin ausgewiesen, haben aber kein Ziel. Allein das verschob den ehrlichen Ausgangswert von 8 % (gesamte Codebasis) auf etwa 24 % (Logik).
2. Die Architektur nach dem Ratchet-Prinzip verbessern
Sieben automatisierte Architekturregeln laufen als Unit-Tests – zum Beispiel keine Android-Typen im Logik-Code und keine globale Dependency Injection. Die heutigen Verstöße sind in einer Baseline-Datei eingefroren: Ein neuer Verstoß lässt den Build fehlschlagen, ebenso ein behobener, bis er aus der Baseline entfernt wird. Die technischen Schulden können nur schrumpfen.
3. Erst Nahtstellen, dann Tests
Jede Abhängigkeit von Android oder einem Drittanbieter – Analytics, Berechtigungen, Biometrie, Screen-Cache, Request-Signierung – bekam ein kleines Interface in der Domain-Schicht und einen Adapter außerhalb der Coverage-Messung. Danach läuft die Logik in Millisekunden auf der reinen JVM, und Tests lassen sich mit wenig Aufwand schreiben.
4. Verhalten festschreiben, nicht stillschweigend korrigieren
Tests der Datenschicht halten fest, was der Code heute tut. Jedes fragwürdige Verhalten wird zu einer Frage an Produkt oder Backend statt zu einer stillen Änderung – bislang 76 offene Fragen. Sobald eine Entscheidung gefallen ist, schlägt der entsprechende Pinning-Test absichtlich fehl und wird zusammen mit dem Fix aktualisiert.
Die Ergebnisse
Alle folgenden Zahlen haben den Stand vom 5. Oktober 2026 und stammen aus der GitLab-Pipeline-API oder wurden in den Repositories gezählt.
| Kennzahl | Vorher | Nachher |
| Repositories mit funktionierendem CI | 0 von 6 | 4 von 6 (SDK + 3 Apps) |
| Vom Merge zu den Testern | Manuelle Builds, manuelles Hochzählen der Version | Etwa 6–8 Minuten, vollständig automatisiert |
| Tester-Builds über die Pipeline | — | 101 seit dem 8. September |
| Pipeline-Läufe | — | Etwa 530 |
| Unit-Tests im SDK | 1.141 (11. September) | 3.246 (×2,8) |
| Logikabdeckung im SDK | Etwa 24 % | 70,9 % |
| Fehlschlagende Tests blockieren Merges | Nein | Ja, im SDK und in der Haupt-App |
| Android-Imports im Logik-Code | 298 | 192 (−36 %) |
| Reproduktion eines Tester-Builds | Nicht möglich | Einen Tag in beiden Repos auschecken |
Die schnellste Phase: Die Logikabdeckung stieg in sieben Tagen (28. September bis 5. Oktober) von 30,7 % auf 70,9 %, als die gesamte Datenschicht in 13 Tickets abgedeckt wurde. Dieser Sprung war nur möglich, weil die Nahtstellen in der Architektur bereits vorhanden waren.
Die Tests haben sich auch schon bezahlt gemacht, bevor das Coverage-Ziel erreicht war. Beim Schreiben der Pinning-Tests kamen Abstürze bei fehlerhaften Serverdaten, Lücken in der Vorwärtskompatibilität älterer App-Versionen und eine zu aggressive Retry-Schleife beim Token-Refresh ans Licht – alles inzwischen erfasst und priorisiert.
Was noch offen ist
Wir veröffentlichen die Lücken lieber zusammen mit den Ergebnissen:
-
Noch keine Coverage-Untergrenze – Merges werden bei fehlschlagenden Tests blockiert, nicht bei sinkender Abdeckung. Die Meilensteine von 40 % und 60 % sind erreicht, 80 % stehen noch aus.
-
Zwei Apps haben noch nicht blockierende Tests und nutzen das gemeinsame Test-Toolkit noch nicht.
-
Zwei Repositories haben kein CI, und End-to-End-Tests laufen noch auf einem lokalen Rechner.
-
Ein einzelner Runner ist ein Single Point of Failure – an zwei Tagen warteten Jobs bis zum Timeout, während er nicht verfügbar war. Ein zweiter Runner oder ein automatisch skalierender Pool ist der nächste Schritt.
Das Vorgehen, das wir jetzt auch bei anderen Kunden nutzen
-
Gemeinsame Abhängigkeiten festlegen und jedes Repo taggen, das an einem Build beteiligt ist, damit sich jeder Tester-Build reproduzieren lässt.
-
Eine Pipeline-Vorlage pro Produktfamilie verwenden, bei der sich die Unterschiede auf eine Handvoll Konstanten beschränken.
-
Den Kreis dort schließen, wo die Leute ohnehin arbeiten – Build-Link und Coverage landen im Jira-Ticket und im Merge Request.
-
Coverage neu definieren, bevor man ihr hinterherjagt, damit die Zahl ehrlich und das Ziel erreichbar ist.
-
Schrittweise nach dem Ratchet-Prinzip statt im Big Bang – die heutigen Verstöße einfrieren und bei allem Neuen fehlschlagen lassen.
-
Einen gemeinsamen Runner als nicht vertrauenswürdig behandeln – Secrets in CI-Variablen, Release-Builds nie aus dem Cache.
Fazit
In zehn Wochen ist eine Fintech-Produktfamilie von handgebauten APKs auf ein einziges CI/CD-System umgestiegen: rund 530 Pipelines, 101 automatisierte Tester-Builds, vom Merge zu den Testern in Minuten und eine SDK-Test-Suite, die von 1.141 auf 3.246 Tests gewachsen ist, bei einer Logikabdeckung von 70,9 %. Dafür war kein neuer CI-Anbieter nötig – nur eine Pipeline, die auf die reale Infrastruktur des Kunden zugeschnitten ist, und eine Architektur, die Tests zulässt.
Bei Mobian zeigen wir diese Art von Arbeit lieber, als sie nur zu beschreiben. Wenn Ihre Releases noch vom Laptop einer einzelnen Person abhängen, können wir mit einem Audit Ihres aktuellen Build- und Release-Prozesses beginnen und Ihnen ehrlich sagen, was sich zuerst zu automatisieren lohnt.
Häufige Fragen
Wie lange hat die Einrichtung von CI/CD gedauert?
Etwa zehn Wochen, von Ende Juli bis Anfang Oktober 2026. Die erste erfolgreiche SDK-Pipeline lief nach rund einem Monat. Tester-Builds über die Pipeline gab es ab Anfang September. Danach dauerte die Übertragung derselben Pipeline auf die anderen Banking-Apps Tage, nicht Wochen.
Warum Logikabdeckung statt Gesamtabdeckung messen?
Die Gesamtabdeckung zählt auch UI-Code, die Verdrahtung der Dependency Injection und generierten Code mit, wo Unit-Tests wenig Nutzen bringen. Die Logikabdeckung zählt nur Domain-, Daten- und Engine-Code, in dem Fehler Geld kosten. Das macht die Zahl ehrlich und das Ziel erreichbar. In diesem Projekt lag der Ausgangswert bei 8,1 % Gesamtabdeckung, aber bei etwa 24 % Logikabdeckung, bevor auch nur ein neuer Test geschrieben war.
Lief die Pipeline auf Cloud-CI?
Nein. Der Kunde betreibt ein selbst gehostetes GitLab mit einem einzigen gemeinsam genutzten macOS-Runner, und darauf wurde die Pipeline ausgelegt: Jeder Job erhält seinen eigenen SDK-Checkout, Secrets liegen ausschließlich in CI-Variablen, und signierte Release-Builds werden nie aus dem gemeinsamen Build-Cache übernommen.
Ist die Arbeit abgeschlossen?
Nicht ganz, und das sagen wir lieber offen. Die Coverage-Untergrenze (ein Merge wird blockiert, wenn die Abdeckung sinkt) ist noch nicht umgesetzt, Tests in zwei der Apps blockieren noch nicht, und der einzelne Runner bleibt ein Risiko für Kapazität und Verfügbarkeit. Der Meilenstein von 80 % Logikabdeckung steht noch aus.
Funktioniert dieser Ansatz auch für ein iOS- oder Cross-Platform-Produkt?
Die Prinzipien lassen sich übertragen: gemeinsame Abhängigkeiten festlegen, jeden Tester-Build taggen, den Code messen, auf den es ankommt, und Tests hinzufügen, sobald die Architektur sie zulässt. Dieses konkrete Projekt war reines Android, unter iOS unterscheiden sich also die Tools (Xcode, Swift Package Manager, XCTest), das Vorgehen bleibt aber dasselbe.