Ich habe ein Update ausgeliefert, das bei Nutzern nicht funktioniert hat. Die Testsuite war vorher komplett grün. Rund 1.100 Tests, alle bestanden, keine Warnung, kein Hinweis.
Das ist der unangenehme Teil am Build in Public: Man schreibt nicht nur über die Dinge, die funktionieren. Um welche App es geht, ist für die Lehre egal, deshalb lasse ich den Namen weg. Der Fehler selbst ist banal. Das Muster dahinter ist es nicht.
Denn das ist die eigentliche Geschichte: Ich hatte sehr viel geprüft und trotzdem genau die eine Stelle nicht, an der etwas kaputtgehen konnte. Und ich habe das nicht bemerkt, weil meine Kennzahl mir Sicherheit vorgespielt hat. Dieses Muster ist nicht spezifisch für Software. Es steckt in jedem Prozess, den ich bei Kunden sehe.
Eine Supportmeldung, die nicht hätte kommen dürfen
Kurz nach dem Update kam die Rückmeldung: „Eingeschaltet, läuft aber nicht."
Der übliche erste Rat half nicht. Neu installieren, neu starten, nichts änderte sich. Und das Unangenehmste daran: Ich hatte kurz vorher bereits eine Korrektur angekündigt. Der Nutzer hatte also nicht nur ein kaputtes Update, sondern auch ein Versprechen von mir im Kopf, das sich gerade als wertlos erwies.
An dieser Stelle ist die technische Ursache erst einmal zweitrangig. Für den Nutzer war die Lage simpel: Das Produkt, für das er bezahlt hat, tut nicht mehr, wofür er es gekauft hat.
Was tatsächlich schiefgelaufen ist
Die App besteht aus zwei Teilen: der eigentlichen Anwendung und einer separaten Systemerweiterung, die die eigentliche Arbeit im Hintergrund erledigt. Beide werden gemeinsam gebaut und gemeinsam ausgeliefert. Die App startet die Erweiterung und spricht sie danach über eine feste Kennung an.
Diese Kennung enthält bei diesem Aufbau die Buildnummer.
Beim Export für den App Store wurde die Buildnummer nachträglich hochgezählt. Die Anwendung wusste danach von der neuen Nummer. Die bereits kompilierte Systemerweiterung trug weiterhin die alte. Die App suchte also an einer Adresse, unter der niemand mehr wohnte.
Das Ergebnis sieht für den Nutzer aus wie ein toter Schalter: Alles wirkt aktiv, es passiert nur nichts. Kein Absturz, keine Fehlermeldung, kein Anhaltspunkt. Genau die Sorte Fehler, die man nicht meldet, sondern bei der man die App löscht.
Warum 1.100 Tests das nicht verhindert haben
Die ehrliche Antwort: Weil meine Tests den falschen Gegenstand geprüft haben.
Eine Testsuite prüft, ob mein Code das tut, was ich beschrieben habe. Sie prüft nicht, ob das Paket, das am Ende beim Nutzer ankommt, dasselbe Paket ist, das ich getestet habe. Das sind zwei verschiedene Fragen, und ich hatte nur die erste beantwortet.
Zwischen Entwicklungsmaschine und Nutzer liegen mindestens drei verschiedene Prüfgegenstände:
| Gegenstand | Was hier getestet wird | Was hier durchrutschen kann |
|---|---|---|
| Entwicklungsbuild | Logik, Verhalten, Randfälle | Alles, was erst beim Signieren und Exportieren entsteht |
| Archiv | Zusammenbau, Abhängigkeiten | Änderungen, die der Export danach noch vornimmt |
| Ausgeliefertes Paket | Genau das, was der Nutzer bekommt | Nichts, wenn man es tatsächlich prüft |
Meine Lücke lag vollständig in der dritten Zeile. Der Fehler wurde nicht beim Kompilieren erzeugt, sondern danach. Kein Test in meiner Suite hatte zu diesem Zeitpunkt überhaupt noch Zugriff auf das Artefakt.
Die Zahl 1.100 hat mir dabei aktiv geschadet. Sie hat mir ein Gefühl von Abdeckung gegeben, das durch nichts gedeckt war. Testanzahl ist kein Qualitätsmaß. Sie sagt aus, wie viel ich geschrieben habe, nicht welche Fehlerklassen ich überhaupt erkennen kann.
Die relevante Frage ist nicht „Wie viele Tests habe ich?", sondern „Welchen Fehler von letzter Woche hätte einer davon gefunden?"
Das eigentliche Learning: Geprüft wird, was bequem ist
Der Bug ist austauschbar. Das Muster nicht, und es begegnet mir in fast jedem Prozess, den ich mir ansehe.
Fehler entstehen an Übergabestellen, geprüft wird aber in der Mitte. Innerhalb eines Bereichs ist Kontrolle leicht: Ich kenne den Code, ich kenne die Daten, ich kenne die Leute. Beim Übergang an den nächsten Schritt hört die Zuständigkeit auf, und genau dort entstehen die teuren Fehler. Bei mir war der Übergang „Code wird Paket". Im Betrieb ist es „Vertrieb übergibt an Projekt", „Angebot wird Auftrag", „System A schreibt nach System B". Jeder prüft sein Stück. Die Naht prüft niemand.
Wir messen Aufwand und halten es für Sicherheit. 1.100 Tests. 40 Seiten Prozessdokumentation. Vier Freigabestufen. Das sind Angaben darüber, wie viel gearbeitet wurde, nicht darüber, welches Risiko abgedeckt ist. Eine Kennzahl, die immer nur steigt und nie unbequem wird, ist keine Kontrolle. Sie ist Dekoration.
Getestet wird der Stellvertreter, nicht das Original. Ich habe meinen Entwicklungsstand geprüft, nicht das Paket beim Nutzer. Das ist dasselbe wie eine Notfallübung auf dem Papier statt im Gebäude, ein Backup, das nie zurückgespielt wurde, oder ein Ablauf, der in der Schulungsumgebung funktioniert. Der Stellvertreter besteht die Prüfung zuverlässig. Er ist ja dafür gebaut.
Die brauchbare Gegenfrage ist immer dieselbe, und sie ist unangenehm:
Welchen der letzten drei realen Fehler hätte meine Kontrolle gefunden? Wenn die Antwort „keinen" ist, prüfe ich die falsche Stelle, egal wie viel ich prüfe.
Der Test, der zählt, ist der, der wehtun kann. Alles andere bestätigt nur, was ich ohnehin geglaubt habe.
Warum „einfach neu installieren" keine Lösung war
Mein erster Reflex war der Rat, den ich selbst nie hören will: Deinstallieren, neu laden, neu einrichten.
Das konnte nicht funktionieren. Aus dem App Store kam exakt dasselbe Paket mit exakt derselben falschen Kennung. Der Nutzer hat also Arbeit investiert, um denselben Fehler ein zweites Mal zu installieren.
Schlimmer: Bei einer Systemerweiterung ist Neuinstallieren nicht kostenlos. Es fallen erneut Berechtigungen an, die bestätigt werden müssen. Und es können Einträge im System zurückbleiben, die mit der alten Installation zu tun haben. Ich habe also Aufwand und ein kleines Aufräumproblem auf jemanden verlagert, der ohnehin schon der Leidtragende war.
Das ist der Teil, der mir am längsten nachgegangen ist. Nicht der Bug. Der Umgang damit.
Was ich konkret geändert habe
Vier Dinge, alle davon langweilig, alle davon hätten den Fehler verhindert.
Buildnummer wird vor dem Kompilieren festgelegt. Sie ist ab dem Zeitpunkt fix. Jede automatische Umnummerierung beim Export ist abgeschaltet. Was in den Build geht, geht auch raus.
Das fertige Paket wird geprüft, nicht der Entwicklungsstand. Eine Checkliste gegen das exportierte Artefakt: Stimmen die Versionskennungen von App und Erweiterung überein? Ist die kompilierte Erweiterung überhaupt enthalten? Sind die Signaturen gültig? Sind alle Übersetzungen drin?
Update und Neuinstallation werden getrennt getestet. Das sind zwei verschiedene Abläufe, und ich hatte faktisch nur einen davon getestet. Dazu gehört auch der hässliche Fall: eine abgebrochene Installation, ein Gerät, auf dem noch Reste einer alten Version liegen.
Getestet wird auf mehreren Geräten. Verschiedene Macs, macOS 26 und 27. Eine saubere Entwicklungsmaschine ist der am wenigsten repräsentative Rechner, den ich besitze.
Keine dieser Maßnahmen ist klug. Sie sind schlicht die Prüfungen, die am Übergang vom Code zum Produkt stattfinden müssen, und genau dort hatte ich nichts.
Was ich künftig anders kommunizieren will
Der technische Teil war in zwei Stunden verstanden. Der Kommunikationsteil war der eigentliche Fehler.
Ich habe „Ursache gefunden" und „Problem gelöst" als dasselbe behandelt. Das sind drei getrennte Zustände, und ich werde sie ab jetzt getrennt benennen:
- Ursache gefunden. Ich weiß, warum es passiert ist.
- Korrektur getestet. Ich habe bei mir nachgewiesen, dass es behoben ist, und kann sagen wie.
- Beim betroffenen Nutzer bestätigt. Der Mensch mit dem Problem sagt, dass es weg ist.
Nur Zustand drei ist eine Lösung. Alles davor ist ein Zwischenstand, und als solcher gehört er formuliert.
Dazu zwei weitere Vorsätze. Ich nenne konkrete Testergebnisse statt Fehlerfreiheit zu versprechen: „Auf zwei Geräten mit Neuinstallation und Update geprüft" ist überprüfbar, „jetzt läuft alles" ist nur Hoffnung mit Ausrufezeichen. Und ich erkläre den technischen Hintergrund erst, nachdem ich die Verantwortung übernommen habe. Eine Erklärung, die vor der Entschuldigung kommt, ist keine Erklärung. Sie ist eine Ausrede mit Fachbegriffen.
Was bleibt
Ein Nutzer hat für ein Werkzeug bezahlt, das nach einem Update aufgehört hat zu arbeiten. Der Grund dafür lag nicht in einem schwierigen technischen Problem, sondern in einer Zahl, die sich an der falschen Stelle geändert hat.
Vertrauen entsteht nicht durch eine große Prüfzahl. Es entsteht dadurch, dass nachvollziehbar ist, was geprüft wurde, und dass die Rückmeldung ehrlich ist, wenn es schiefgeht.
Die 1.100 Tests bleiben übrigens. Sie waren nie das Problem. Sie waren nur nie das Argument, für das ich sie gehalten habe.
Wenn du in deinem eigenen Ablauf eine Stelle suchst, an der das genauso liegt: Nimm den letzten Fehler, der es bis zum Kunden geschafft hat, und frag dich, an welcher Übergabe er entstanden ist. Dort steht deine Lücke. Nicht dort, wo du heute misst.