In einem Regal steht ein Apple M4 Mac mini mit 16 GB Speicher, der macOS 15, Xcode 26.6 und Swift 6.3.3 laufen lässt und nur über SSH im lokalen Netzwerk erreichbar ist. Am 5. September 2026 verbrachte er einen Nachmittag damit, die Interface-Tests für beide unserer Mac-Apps hintereinander laufen zu lassen, während die Bildschirmaufnahme lief. MeatPad, unser Notizbuch und Code-Editor für macOS, ist eine mit XCUITest getestete SwiftUI-App: 50 Fälle, beginnend um 12:58. MailVault, unser lokaler E-Mail-Archivierer, ist eine mit WebdriverIO getestete Tauri-App: 539 Fälle, beginnend um 14:43. Das sind 589 Testfälle vor der Kamera, auf einer Maschine, an einem Nachmittag, und die beiden Läufe verhalten sich überhaupt nicht ähnlich.
Akt eins: MeatPad, fünfzig Starts in 405 Sekunden
Ein Befehl, auf der linken Seite des Bildschirms, gegen MeatPad 0.15.0:
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
-only-testing:MeatPadUITests
Alles andere in dieser Aufnahme ist eine Folge dieser Zeile. Xcode baut die App und ein zweites Bündel namens Test-Runner, und übergibt beide an die Maschine. Für jeden Testfall startet der Runner die App von Grund auf neu, wartet auf ihr Fenster, bewegt den Zeiger, drückt Tasten, liest, was zurückkommt, und beendet die App. Dann macht er das Gleiche noch einmal für den nächsten Fall. Die Fenster, die erscheinen und verschwinden, sind kein visueller Effekt. Das ist die Schleife.
Warum ein XCUITest-Fall Sekunden kostet
Der Lauf hat 50 Fälle in 405.032 Sekunden abgeschlossen. Einer der 50 überspringt sich absichtlich selbst, sodass die 49 tatsächlich ausgeführten im Schnitt etwa 8.3 Sekunden brauchen. Zum Vergleich: bei MeatPadKit, dem Framework unter der Oberfläche, dauert ein Test im Schnitt neunzehn Millisekunden.
Drei Dinge kosten diesen Unterschied. Die App wird pro Fall einmal gestartet, also zahlt jeder Fall für einen Kaltstart und dafür, dass sich das Fenster einpendelt. Der Test läuft in einem separaten Prozess und kann die App nur über den macOS-Accessibility-Baum sehen, also ist jede Frage zum Bildschirm eine Abfrage über eine Prozessgrenze hinweg statt eines Speicherzugriffs. Und Zeiger und Tastatur sind echt, also dauert ein Drag so lange wie ein Drag.
Fünfzig Wegwerf-Welten
Ein Fall, der auf eine Karte klickt, braucht eine Karte zum Klicken. In den laufenden Prozess wird nichts injiziert, also muss der Zustand existieren, bevor die App startet. Jeder Fall schreibt ein frisches Speicherverzeichnis in den temporären Systemordner, sät ein Board hinein und startet dann die App darauf ausgerichtet:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Diese drei Flags sind echter Code im Release-Binary. Sie existieren, damit ein Test einen Bildschirm erreichen kann, und sie werden an alle ausgeliefert. Das ist der ehrliche Preis dafür, eine native Oberfläche von außen zu testen, und wir würden ihn wieder zahlen, aber er sollte laut ausgesprochen werden, statt von jemandem beim Lesen des Quellcodes entdeckt zu werden. Der Vorteil ist, dass der Lauf nichts zurücklässt: Jeder Fall löscht sein Verzeichnis im Teardown, sodass Ihre eigenen Boards nie berührt werden und ein Fehlschlag den nächsten Fall nicht vergiften 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 →Die Uhr, Suite für Suite
MeatPad 0.15.0 · MeatPadUITests · Apple M4 Mac mini · 2026-09-05
BoardCardDisplay 4 cases 49.973 s
BoardCardEditor 9 cases 68.393 s
BoardCardFace 5 cases 31.313 s
BoardCardLink 2 cases 14.380 s
BoardDrop 3 cases 30.463 s
BoardLabel 8 cases 102.638 s
BoardNewline 5 cases 26.818 s
BoardSearch 5 cases 36.664 s
BoardShot 1 skipped 0.020 s
NamePrompt 1 case 6.943 s
NoteLink 1 case 17.615 s
OpenWith 6 cases 19.812 s
total 50 cases 405.032 s
Zwölf Suiten, und jede ist nach dem benannt, was sie absichert: die drei Kartenanzeige-Modi, der Karten-Editor mit seinen Farbmustern und Fälligkeitsdaten, das Bearbeiten des Titels an Ort und Stelle und das Ziehen einer Karte zwischen Spalten, Links innerhalb von Kartennotizen, Drop-Ziele, Labels und Label-Filterung, Zeilenumbrüche im Schnellerfassungsfeld, Suche, der Tastaturfokus des neuen Board-Sheets, der Link-Hinweis des Notizeditors, und die sechs Fälle, die Launch Services fragen, ob MeatPad für jeden Dateityp angeboten wird, den es beansprucht.
BoardLabel ist mit 102.638 Sekunden für 8 Fälle der teuerste, weil das Filtern nach Labels Karten ein bis zwei Frames nach dem Tastendruck aus der Ansicht entfernt, sodass diese Fälle in einer Polling-Schleife sitzen und darauf warten, dass sich die Spalte einpendelt. Der langsamste einzelne Fall im Lauf dauerte 27.992 Sekunden und prüfte, ob ein langer Kartentitel in der kompakten Anzeige abgeschnitten wird und in den anderen beiden umbricht, was bedeutet, denselben Titel dreimal in drei Starts zu messen. Der schnellste Fall dauerte 1.507 Sekunden und fragte Launch Services, ob MeatPad für jeden registrierten Dateityp angeboten wird, was die laufende App braucht, aber nie ihr Fenster berührt.
Die Zusammenfassungszeile, die Sie anlügt
Das Xcode-Projekt von MeatPad wird aus einem Manifest generiert und ist nicht eingecheckt. Das ist praktisch, bis zu dem Tag, an dem Sie eine Testdatei hinzufügen, das Neugenerieren vergessen und die Suite trotzdem laufen lassen. Aus Sicht von xcodebuild ist nichts falsch. Es kompiliert das Projekt, das ihm gegeben wurde, führt die null Tests darin aus, und druckt die zwei Wörter, nach denen jeder Ausschau hält:
** TEST SUCCEEDED **
Ein Lauf, der nichts ausgeführt hat, sieht exakt so aus wie ein Lauf, der alles ausgeführt hat. Die Zeile, die es zu lesen lohnt, ist deshalb nie die letzte. Es ist die Zahl darüber, die sagt, wie viele Fälle tatsächlich passiert sind. Hier sind die letzten Zeilen des Laufs aus der Aufnahme oben. Das Terminal im Video leitet xcodebuild durch ein grep und ein sed, also druckt es eine gekürzte Form derselben Ausgabe, und die Zeilen passen neben das App-Fenster. Der Befehl selbst ist unverändert:
[OpenWithUITests]
testAMultiSelectionOpensEveryFileAsATab passed 3.799s
testASecondFileJoinsTheWindowThatIsAlreadyOpen passed 4.066s
testLaunchServicesOffersMeatPadForEveryFileTypeItClaims passed 1.507s
testOpeningAFileShowsItAsATabInAProjectWindowForItsFolder passed 3.825s
testOpeningAFolderOpensItAsTheProjectWithNoTabs passed 3.950s
testOpeningAnExtensionlessFileOpensItToo passed 2.665s
Executed 6 tests, with 0 failures in 19.812 s
Executed 50 tests, with 1 test skipped and 0 failures in 405.032 s
All tests passed
Executed 50 tests, with 1 test skipped and 0 failures in 405.032 s
** TEST SUCCEEDED **
Fünfzig Fälle, einer absichtlich übersprungen, keine Fehlschläge, 405.032 Sekunden. Die Zahl und die letzte Zeile stimmen überein, und das ist die einzige Kombination, der man vertrauen sollte.
Was vor der grünen Aufnahme geschah
Der aufgezeichnete Lauf war nicht der erste des Tages. Ein nicht aufgezeichneter um 12:19 ging rot: dieselben 50 Fälle, 1 übersprungen, und 2 Fehlschläge, beide in OpenWith, der Fall mit der Mehrfachauswahl und der Fall mit der zweiten Datei, die beiden, die Sie im Ausschnitt oben bei 3.799 und 4.066 Sekunden erfolgreich sehen können. Die Absturzberichte sagen, dass die App 0.75 Sekunden nach dem Start abgebrochen ist, innerhalb von SwiftUIs eigenem Startpfad, bei einem AttributeGraph-Präconditions-Fehler, ausgelöst aus applicationWillFinishLaunching, während der Dock die App noch benachrichtigte. Es war ihr noch nichts übergeben worden, also ist das nicht der Code zum Öffnen von Dateien. Dieselben 6 OpenWith-Fälle, direkt danach für sich allein ausgeführt, bestanden 3 von 3 Mal, was 18 von 18 Fällen entspricht, und alle 6 bestanden im aufgezeichneten Lauf erneut. Es sieht nach einem Timing-Flake beim Start aus, der nur auftritt, wenn die Suite die App unmittelbar hintereinander neu startet. Es wird noch untersucht, und es wird nichts herausgeschnitten.
Eine Sache, die beim Einrichten auffiel, verdient einen eigenen Absatz. Um die App neben dem Terminal unterzubringen, haben wir MeatPads gespeicherten Fensterrahmen zunächst von 1440x900 Punkten auf 1010x900 verkleinert. Vier Fälle schlugen auf einmal fehl: die Farbmuster im Karten-Editor, ein Label-Filter und eine Suche, alle rein deshalb, weil das Element, nach dem sie griffen, außerhalb des Bildschirms lag. Erneut mit dem ursprünglichen 1440x900-Rahmen ausgeführt, bestand dieselbe Gruppe 9 von 9. Die Fenstergröße ist eine Eingabe für einen UI-Test, und diese Suite geht stillschweigend vom Standardrahmen der App aus. Deshalb zeigt die Aufnahme MeatPad in seiner Standardgröße rechts und ein schmales Terminal links.
Zwei frühere Aufnahmen landeten im Papierkorb, aus Gründen, die nichts mit MeatPad zu tun hatten: in der einen ein veralteter Absturzdialog, der vom Lauf um 12:19 übrig geblieben war und auf dem Bildschirm stand, und in der anderen legten sich auf halbem Weg die eigenen Fenster der MailVault-Suite aus einem anderen Job darüber, weil der mini eine gemeinsam genutzte Maschine ist. Was eine passende Überleitung zur zweiten Hälfte des Nachmittags ist.
Akt zwei: MailVault, 81 Spec-Dateien in sechzehn Minuten
MailVault 2.11.3 ist auf die andere Art gebaut: ein Rust-Kern mit einem Web-Frontend, verpackt von Tauri. Ihre End-to-End-Suite wird von WebdriverIO gesteuert, und der Befehl um 14:43 forderte zwei ihrer drei Suiten an:
npx wdio run wdio.conf.js --suite ui-headless --suite connected-ci
Die Suite ui-headless umfasst 7 Spec-Dateien, die den Willkommenszustand ohne konfigurierte Konten abdecken. Die Suite connected-ci umfasst 74 Spec-Dateien, die gegen bestückte Mock-IMAP-Konten laufen. Das sind 81 Spec-Dateien. Eine dritte Suite, local-manual, enthält 6 weitere Spec-Dateien für Backup, Migration, Archiv und visuelle Prüfungen, und war nicht Teil dieses Laufs.
Spec Files: 80 passed, 1 failed, 81 total (100% completed) in 00:16:03
Achtzig Spec-Dateien bestanden, eine schlug fehl, 81 insgesamt, in 16 Minuten und 3 Sekunden nach wdios eigener Uhr. Das Video läuft 16:18, weil es vor dem Befehl beginnt und danach endet. Unter diesen Dateien liegen 539 Testfälle: 533 bestanden, 5 übersprungen und der eine Fehlschlag. Die Hälfte ui-headless steuerte 55 bestandene und 3 übersprungene in etwa 56 Sekunden Testzeit bei, und connected-ci steuerte 478 bestandene, 1 fehlgeschlagenen und 2 übersprungene in etwa 811 Sekunden bei.
Die App wird einmal pro Spec-Datei gestartet statt einmal pro Fall, sodass die 539 MailVault-Fälle des Nachmittags 81 Starts kosten. Eine Spec-Datei braucht im Schnitt etwa 10.7 Sekunden, und der Median liegt bei 6 Sekunden, was zeigt, dass der Durchschnitt von einer Handvoll langer Dateien getragen wird: connected-custody-claims mit 54.3 Sekunden für 9 Tests, connected-performance mit 53.4 Sekunden für 7, connected-compose-editor mit 39.6 Sekunden für 19, connected-compose-autosave mit 38.7 Sekunden für 15. Die schnellste Datei, connected-backup-partial-failure, führte ihre 5 Tests in 32 Millisekunden aus. Die geschäftigste, connected-email-viewer, enthält 21 Tests. All das liegt auf einer viel größeren und viel günstigeren Basis: 2,683 vitest-Unit-Tests in 216 Dateien sind in 6.77 Sekunden fertig, 318 Rust-Tests decken den Kern ab, und die beiden aufgezeichneten Suiten sind 518 it()-Fälle im Quellcode.
Zwei Dinge in der Aufnahme verdienen ein Wort. Das Terminal links zeigt nur wdios RUNNING-, PASSED- und FAILED-Zeilen pro Datei, weil wdio seinen detaillierten Bericht bis ganz zum Ende des Laufs aufhebt. Und gegen Ende öffnet die Export-Spec eine exportierte Nachricht in Preview, was das Terminal für die letzte Minute verdeckt; das ist der Test, der seinen Job macht, kein Unfall.
Was das Testgerüst aufsetzt
Jeder Lauf startet seine eigenen Mock-IMAP-Server mit drei bestückten Konten, luke, vader und yoda bei mock.test, und vaders INBOX enthält 700 Nachrichten, komfortabel mehr als die Ladefenster der App, sodass Paginierung tatsächlich geprüft wird statt nur angenommen. Ein viertes Konto wird von einem Test hinzugefügt, dem Sie beim Eintippen zusehen können. Die getestete App ist ein Build mit aktiviertem webdriver-Feature, gestartet von tauri-wd, der tauri-webdriver-automation-Crate, und läuft gegen ein Wegwerf-HOME-Verzeichnis, sodass ein echter Vault nie berührt wird. Dieses Testgerüst hat seine eigene Beschreibung in Feldnotiz 004.
Der eine Fehlschlag, unklassifiziert
Einer von 539 Fällen schlug fehl: die Spec connected-storage-matrix, Test „a row deleted in unified mode stays gone across account churn and a reload". Ihr Helfer switchToUnified prüft sofort und ohne Wartezeit, dass die Schaltfläche „Alle Posteingänge“ in der Seitenleiste existiert und sichtbar ist, und läuft direkt, nachdem der vorherige Test die App neu geladen hat. Erwartet true, erhalten false. Wir konnten diese einzelne Spec danach nicht erneut ausführen, weil jeder Versuch, in diesem Checkout eine frische Driver-Sitzung zu starten, scheiterte, bevor er den Driver erreichte, mit einem undici UND_ERR_INVALID_ARG bei der Session-Anfrage. Also bleibt er unklassifiziert. Es liest sich eher wie eine fehlende Wartezeit nach einem Reload als wie ein Produktfehler, und er bleibt im Video.
1.8 Sekunden pro Fall gegenüber 8.3
539 MailVault-Fälle in 963 Sekunden Laufzeit sind etwa 1.8 Sekunden pro Fall. MeatPads 49 ausgeführte Fälle in 405.032 Sekunden sind etwa 8.3 Sekunden je Fall. Gleiche Maschine, gleicher Nachmittag, ungefähr ein Faktor fünf zwischen beiden.
Zwei strukturelle Gründe, keine einzelne clevere Optimierung. WebDriver führt JavaScript innerhalb der laufenden Anwendung aus und liest das DOM direkt, sodass eine Prüfung gegen einen der 494 data-testid-Haken in den End-to-End-Specs aufgelöst wird, statt eine Prozessgrenze in den macOS-Accessibility-Baum zu überqueren. Und die Rechnung bei den Starts unterscheidet sich: 539 MailVault-Fälle kosten 81 App-Starts, während 50 MeatPad-Fälle 50 kosten. Diese Lücke haben wir vor ein paar Wochen ordentlich gemessen, in Feldnotiz 006.
Nichts davon macht die eine App besser getestet als die andere. Die Suiten prüfen unterschiedliche Produkte, und 8.3 Sekunden pro Fall sind der Preis dafür, eine native Oberfläche auf die einzige Art zu steuern, die macOS von außerhalb des Prozesses anbietet.
Selbst ausprobieren
git clone https://github.com/GraphicMeat/MeatPad
cd MeatPad
brew install xcodegen
xcodegen generate
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
-only-testing:MeatPadUITests
Überspringen Sie die xcodegen-Zeile nicht, aus dem oben genannten Grund. Der MeatPad-Quellcode ist auf GitHub. Die Suite von MailVault ist ein weiterer Klon und derselbe Befehl, der vor der Kamera lief:
git clone https://github.com/GraphicMeat/mail-vault-app
cd mail-vault-app
npm install
npx wdio run wdio.conf.js --suite ui-headless --suite connected-ci
Das alles ist öffentlich, den MeatPad-Flake und den einen MailVault-Fehlschlag eingeschlossen, und der MailVault-Quellcode liegt neben dem von MeatPad auf GitHub.
Local-first Mac-Software, mit den Messungen im Anhang. MailVault hält Ihre Post in einem Tresor auf Ihrer eigenen Festplatte, und eine Maschine klickt sich vor jedem Release durch die gesamte App.
MailVault ansehen →