Unter dem Schreibtisch steht ein Apple M4 Mac mini mit 16 GB Speicher, und seine Aufgabe ist herauszufinden, ob unsere Apps noch funktionieren. MailVault wird mit Tauri gebaut. MeatPad wird mit SwiftUI gebaut. Der mini führt beide Suites aus: dieselbe Maschine, derselbe Nachmittag, dieselbe Person, die die Tests schreibt.
Die beiden Suites kosten nicht dasselbe. Sie kosten nicht annähernd dasselbe. Alles Folgende wurde am 25. August 2026 gemessen, und jeder Befehl ist einer, den Sie selbst ausführen können.
Was heute grün durchlief
MailVault 2.10.2 · Tauri (Rust core + web frontend)
unit and component npm test 1,218 tests 86 files 2.28 s
integration npm run test:integration 64 tests 8 files 2.91 s
core and daemon npm run test:imap 158 tests 1.4 s
end to end npm run test:e2e 53 spec files
7 headless · 41 seeded with mock IMAP · 5 local only
MeatPad 0.13.0 · SwiftUI (AppKit, TextKit 2)
MeatPadKit swift test 601 tests 41 files 11.47 s
interface xcodebuild test 31 tests 5 files
Lesen Sie die letzten beiden Zeilen jedes Blocks gegeneinander, denn darin steckt der ganze Artikel. MailVault bekommt 1.440 Prüfungen ohne Fenster in unter sieben Sekunden Laufzeit, und es kann zusätzlich seine eigene echte Binärdatei durch 53 End-to-End-Specs steuern. MeatPad bekommt 601 Prüfungen ohne Fenster in elf Sekunden und 31 Tests, die die tatsächliche Oberfläche berühren.
Dann gibt es noch die Zahl, die nicht in eine Tabelle passt. Acht dieser 31 Oberflächentests auf dem mini auszuführen dauerte 98,186 Sekunden. Einzelne Fälle lagen zwischen 6,9 und 17,2 Sekunden. Nennen wir es zwölf Sekunden pro Test.
Executed 8 tests, with 0 failures (0 unexpected) in 98.186 seconds
Ein MeatPadKit-Unit-Test braucht auf derselben Maschine im Schnitt neunzehn Millisekunden. Ein MailVault-vitest-Fall im Schnitt unter zwei. Ein einziger simulierter Klick auf eine SwiftUI-View kostet etwa sechshundert Unit-Tests.
August, in zwei Kurven
MailVault hat den Monat damit verbracht, Tests geschrieben zu bekommen, und die Form davon sieht man leicht, wenn man die Testfälle im Repository Woche für Woche zählt:
MailVault, JavaScript test cases and end-to-end spec files
1 March 440 cases
1 August 850 cases 22 e2e specs
8 August 972 cases 28 e2e specs
15 August 1,111 cases 30 e2e specs
22 August 1,393 cases 43 e2e specs
25 August 1,604 cases 53 e2e specs
Die Zahl der Fälle hat sich in fünfundzwanzig Tagen fast verdoppelt, und die End-to-End-Specs haben sich mehr als verdoppelt. Dieses Wachstum verteilt sich nicht gleichmäßig über die App. Es ging dorthin, wo die Fehler waren: Verfassen und Entwürfe, Mehrfachauswahl, die Speichermatrix, die sich nach jeder verändernden Aktion darüber einig sein muss, was auf der Platte liegt, und der Kontowechsel. Die Commit-Titel aus dieser Strecke lesen sich wie eine Liste von Geständnissen, und genau so soll sich ein Testprotokoll lesen.
Die Kurve von MeatPad ist kürzer, weil die App jünger ist, und sie hat eine Stufe, über die man ehrlich sprechen sollte:
MeatPad, Swift test functions
18 July 327 tests 0 through the interface
21 July 517 tests 0 through the interface
19 August 548 tests 0 through the interface
25 August 632 tests 31 through the interface
MeatPad erschien im Juli mit 517 Tests, und nicht einer davon öffnete ein Fenster. Der erste Oberflächentest wurde am 24. August geschrieben, einen Monat nach der Veröffentlichung. Diese Lücke ist keine Faulheit. Sie ist das Thema des restlichen Artikels.
Ein E-Mail-Archiv, das Sie wirklich prüfen können. MailVault sichert und archiviert Ihr Postfach lokal, und 53 End-to-End-Specs steuern die echte Anwendung, bevor irgendetwas davon Sie erreicht.
Besuchen Sie MailVault →Der Unterschied, der alle anderen erklärt
Die Oberfläche einer Tauri-App ist eine Webseite. Das Testgerüst spricht WebDriver mit ihr, was bedeutet, dass das Gerüst Code innerhalb der laufenden Anwendung selbst ausführen kann:
const searchExists = await browser.execute(() => {
const input = document.querySelector('input[placeholder*="Search"]');
return input !== null;
});
Diese Pfeilfunktion wird nicht vom Test ausgewertet. Sie wird in die eigene Webview der App geschickt, dort gegen das lebende Dokument ausgeführt, und ihr Ergebnis kommt zurück. Der Test kann die Oberfläche genau so befragen, wie die Anwendung selbst es kann.
XCUITest kann das nicht und wird es nie können. Es ist ein eigener Prozess, der Ihre App von außen über die macOS-Accessibility-Schicht steuert. Es gibt keinen Kanal für beliebigen Code, keine Möglichkeit, einer View eine Frage zu stellen, die der Accessibility-Baum nicht ohnehin beantwortet. Alles Weitere ist eine Folge dieser einen Grenze.
In einem Dokument hat alles einen Griff
Die End-to-End-Suite von MailVault wählt Elemente 305-mal über Test-ID-Attribute aus. Eines hinzuzufügen kostet nichts und ändert nichts daran, wie die App läuft. Alles, was die App rendert, ist adressierbar, ob das jemand so geplant hat oder nicht.
MeatPad hat 34 Accessibility-Identifier im gesamten Quellbaum, und jeder einzelne musste bewusst an eine View im ausgelieferten Produktionscode gehängt werden:
TextField("Filter", text: $query)
.accessibilityIdentifier("board.labelFilter")
Alles ohne einen solchen ist für Testzwecke nicht vorhanden. Die Testfläche einer SwiftUI-App ist nicht „die Oberfläche“. Sie ist „die Teile der Oberfläche, die jemand zu benennen dachte“, und diese Liste wächst nur, wenn gerade ein Test geschrieben wird.
Und Sie können nicht fragen, was für ein Ding Sie benannt haben
Selbst mit einem Identifier verspricht Ihnen eine SwiftUI-View nicht, was aus ihr geworden ist. So erreichen die Tests von MeatPad das Suchfeld des Boards:
app.descendants(matching: .any).matching(identifier: "board.search").firstMatch
Auf einen beliebigen Nachfahrentyp zu matchen ist eine Kapitulation. Im Quelltext steht TextField, aber ob es als Textfeld, als Suchfeld oder als etwas ganz anderes auftaucht, hängt davon ab, wie SwiftUI es an diesem Tag gebaut hat, und das kann sich mit einem Betriebssystem-Update ändern. Jeden Elementtyp zu durchsuchen ist langsamer und vager, als nach dem zu fragen, was Sie geschrieben haben, und es ist die einzige verlässliche Option.
Den Wert wieder auszulesen ist dieselbe Geschichte. Ein Kartentitel kommt als optionale Umwandlung über einen Wert unbekannten Typs zurück, weil die Accessibility-Schicht nicht mehr anbietet, und richtig zu raten ist Ihre Aufgabe.
Niemand wartet auf Sie
WebDriver hat das Warten eingebaut. XCUITest wartet darauf, dass ein Element existiert, und bietet überhaupt nichts für eine Bedingung über mehrere hinweg. Also bauen die Tests von MeatPad es selbst:
private func waitForCardTitles(_ expected: [String]) -> Bool {
let deadline = Date().addingTimeInterval(10)
while Date() < deadline {
if visibleCardTitles == expected.sorted() { return true }
usleep(200_000)
}
return false
}
Karten verlassen die View-Hierarchie ein oder zwei Frames nach dem Tastendruck, der sie herausgefiltert hat, deshalb ist eine einzelne Zusicherung direkt nach dem Tippen ein Münzwurf. Jeder Test, der nach einer Interaktion eine Liste prüft, braucht diese Schleife, und jede dieser Schleifen ist eine Stelle, an der ein echter Fehler zehn Sekunden lang wie ein langsamer Erfolg aussehen darf.
Die App in einen bekannten Zustand bringen
Die Suite von MailVault startet pro Konto einen Mock-IMAP-Server mit einem vorbereiteten Postfach mit 700 Nachrichten, richtet die App darauf aus und gibt dem ganzen Lauf ein Wegwerf-Home-Verzeichnis. Die getestete Anwendung ist vollkommen unverändert. Sie ahnt nicht, dass sie getestet wird.
MeatPad muss man es sagen. Seine UI-Tests schreiben ein Speicherverzeichnis auf die Platte und starten die App dann mit Argumenten, die die ausgelieferte Binärdatei versteht:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Diese Flags sind echter Code in der veröffentlichten Anwendung und existieren, damit Tests einen Bildschirm erreichen können. Das ist ein fairer Handel, und wir würden ihn wieder eingehen, aber man sollte es beim Namen nennen: Mit XCUITest wird ein Teil Ihres Testgerüsts an die Nutzer ausgeliefert.
Wo die Tests laufen dürfen
Die fensterlose End-to-End-Suite von MailVault läuft auf einem gewöhnlichen Ubuntu-Runner von GitHub Actions. Die echte Tauri-Binärdatei wird für Linux gebaut, in einem virtuellen Framebuffer und einer Message-Bus-Sitzung gestartet und von WebDriver auf Hardware durchgeklickt, die pro Minute nichts kostet. Die genaue Beschwörungsformel steht unten.
dbus-run-session -- xvfb-run --auto-servernum \
--server-args="-screen 0 1920x1080x24" \
npx wdio run wdio.conf.js --suite ui-headless
Für XCUITest gibt es keine entsprechende Zeile. Es braucht macOS, einen Window-Server und eine echte Benutzersitzung, weil es tatsächlich einen Zeiger bewegt und Tasten drückt. Das Repository von MeatPad hat genau einen GitHub-Workflow, und der baut Releases. Die Tests leben auf dem mini, was die ehrliche Antwort darauf ist, wo ein kleines Studio Mac-UI-Tests laufen lässt, und der Grund, warum es diese Maschine überhaupt gibt. Wie sie sich für MailVault bezahlt macht, haben wir beschrieben in einer früheren Notiz.
Der Teil von MeatPad, den kein UI-Test je erreichen wird
Schauen Sie noch einmal, wo die 31 Oberflächentests von MeatPad sitzen: Kartendarstellung, Karteneditor, Labels, Zeilenumbrüche, Suche. Alles davon ist das Board. Nicht einer berührt den Editor, der das eigentliche Produkt ist.
Der Editor umschließt STTextView auf TextKit 2 in einem NSViewRepresentable, und für den Accessibility-Baum ist er eine einzige undurchsichtige Region mit Text darin. Mehrere Cursor, Snippet-Platzhalter, Code-Faltung, inkrementelles Highlighting mit tree-sitter, Klammerabgleich: nichts davon existiert als abfragbare Elemente. Sie können nicht zusichern, dass der zweite Cursor auf Zeile vierzig gelandet ist, weil es kein Element namens „der zweite Cursor“ gibt.
Also wird diese Arbeit nicht über die Oberfläche getestet. Sie wird darunter getestet. MeatPadKit ist ein eigenes Swift-Paket mit 47 Quelldateien und enthält den Fold-Scanner, das Multi-Cursor-Modell, den Snippet-Parser, das Highlighting, den Fuzzy-Matcher, die LSP-Positionsbrücke und den Projekt-Symbolindex. Es enthält keinen View-Code, und genau deshalb können 601 Tests in elf Sekunden dagegen laufen, ohne dass ein Fenster auf dem Bildschirm erscheint.
Der Kommentar am Anfang eines der UI-Tests von MeatPad bringt die Aufteilung besser auf den Punkt, als wir sie umschreiben könnten: MeatPadKit testet den Matcher selbst im Unit-Test, und was es nicht erreichen kann, ist, ob das Feld überhaupt mit den Spalten verdrahtet ist. Das ist die vollständige Stellenbeschreibung eines SwiftUI-UI-Tests. Nicht „funktioniert die Logik“, was ein schneller fensterloser Test bereits beantwortet hat, sondern „ist es angeschlossen“, was nichts anderes beantworten kann.
Starten Sie mit einer Notiz. Bleiben Sie wegen des Editors. MeatPad ist ein nativer macOS-Notizblock und Code-Editor: einfache Dateien auf Ihrem Mac, kein Konto, keine Sync-Engine, keine Telemetrie.
MeatPad ansehen →Was das tatsächlich daran ändert, wie Sie bauen
Die Lehre ist nicht, dass SwiftUI schlecht ist. Sie ist, dass die Kosten für das Überprüfen einer nativen Oberfläche hoch genug sind, um eine architektonische Eingangsgröße zu sein statt eines nachträglichen Gedankens.
- Schieben Sie Logik aus der View heraus, bis die View langweilig ist. MeatPad besteht aus 51 Quelldateien Oberfläche und 47 Framework, und die Framework-Seite trägt 95 Prozent der Tests. Alles, was in einer SwiftUI-View bleibt, ist teuer zu prüfen, also ist die richtige Menge, die dort bleibt, das Layout.
- Geben Sie Oberflächentests für die Verdrahtung aus, nicht für das Verhalten. Zu zwölf Sekunden pro Stück sind sie das falsche Werkzeug für Randfälle und das richtige für „das Suchfeld ist mit der Spalte verbunden“.
- Fügen Sie Identifier hinzu, wenn Sie die View schreiben, nicht wenn Sie den Test schreiben. Sie nachzurüsten bedeutet, Produktionsdateien zu bearbeiten, um einen Test möglich zu machen, und genau dann hören Leute auf, Tests zu schreiben.
- Wenn Ihre Oberfläche eine Webview ist, nehmen Sie das kostenlose Mittagessen. Eine Abfrage in der eigenen laufenden Anwendung ausführen zu können ist ein echter Vorteil des Tauri-Stacks, und deshalb konnte MailVault in einem einzigen Monat 31 End-to-End-Specs ergänzen.
Was nicht abgedeckt ist
Zwei ehrliche Lücken, denn ein Artikel über Tests, der nur die Erfolge meldet, ist eine Werbebroschüre.
Der Editor von MeatPad hat überhaupt keine Abdeckung auf Oberflächenebene. Die Logik darunter ist gründlich getestet, und der Kitt zwischen dieser Logik und der Textview wird von einem Menschen geprüft. Eine Regression dort würde ein Release erreichen. Das mit einer kleinen Zahl teurer Tests abzutragen, bei den Interaktionen, die zwölf Sekunden wert sind, steht als Nächstes an.
Auf der Seite von MailVault läuft nur die fensterlose Suite in der Continuous Integration. Die 41 vorbereiteten Specs und die 5 lokalen laufen auf dem mini, auf Zuruf, was bedeutet, dass sie Dinge vor einem Release abfangen statt vor einem Merge. Und eine grüne Suite beweist immer noch nur die Dinge, an deren Prüfung jemand gedacht hat, weshalb die Zahl in jener Tabelle weniger zählt als die Tatsache, dass sie sich weiterbewegt.
Alles davon nachvollziehen
# MailVault
git clone https://github.com/GraphicMeat/mail-vault-app
npm install
npm test # 1,218 unit and component tests
npm run test:integration # 64 integration tests
npm run test:imap # 158 Rust tests, core and daemon
npm run test:e2e # builds the app, drives it through WebDriver
# MeatPad
cd MeatPadKit && swift test # 601 tests, no window required
xcodebuild test -project MeatPad.xcodeproj -scheme MeatPad \
-destination "platform=macOS" -only-testing:MeatPadUITests
Der letzte Befehl ist der, der zwei Minuten dauert. Jetzt wissen Sie, warum.
Zwei Apps, eine Maschine, die sie ehrlich hält. Local-first Mac-Software von einem Studio, das seine Messungen veröffentlicht, auch die unvorteilhaften.
Sehen, was wir bauen →