C'è un Apple M4 Mac mini con 16 GB di memoria su uno scaffale, con macOS 15, Xcode 26.6 e Swift 6.3.3, raggiungibile solo via SSH sulla rete locale. Il 5 settembre 2026 ha passato un pomeriggio a eseguire i test di interfaccia per entrambe le nostre app Mac, una dopo l'altra, con lo schermo registrato. MeatPad, il nostro blocco note e editor di codice per macOS, è un'app SwiftUI testata con XCUITest: 50 casi, a partire dalle 12:58. MailVault, il nostro archiviatore di email locale, è un'app Tauri testata con WebdriverIO: 539 casi, a partire dalle 14:43. Sono 589 casi di test ripresi in video, su una sola macchina, in un pomeriggio, e le due esecuzioni non si comportano affatto allo stesso modo.

Atto primo: MeatPad, cinquanta avvii in 405 secondi

Guarda su YouTube

Un comando, a sinistra dello schermo, contro MeatPad 0.15.0:

xcodebuild test -scheme MeatPad -destination "platform=macOS" \
  -only-testing:MeatPadUITests

Tutto il resto in quella registrazione è una conseguenza di quella riga. Xcode compila l'app e un secondo bundle chiamato test runner, poi consegna entrambi alla macchina. Per ogni caso di test il runner avvia l'app da zero, attende la sua finestra, muove il puntatore, preme tasti, legge cosa è tornato indietro, e termina l'app. Poi lo rifà per il caso successivo. Le finestre che appaiono e scompaiono non sono un effetto visivo. Quello è il ciclo.

Perché un caso XCUITest costa secondi

La sessione ha finito 50 casi in 405.032 secondi. Uno dei 50 si salta da solo di proposito, quindi i 49 effettivamente eseguiti fanno una media di circa 8.3 secondi ciascuno. Confrontalo con MeatPadKit, il framework sotto l'interfaccia, dove un test fa una media di diciannove millisecondi.

Tre cose comprano quella differenza. L'app viene avviata una volta per ogni caso, quindi ogni caso paga un avvio a freddo e l'assestamento della finestra. Il test gira in un processo separato e può vedere l'app solo attraverso l'albero di accessibilità di macOS, quindi ogni domanda sullo schermo è una query attraverso un confine di processo invece di una lettura di memoria. E il puntatore e la tastiera sono reali, quindi un trascinamento richiede tanto quanto un trascinamento.

Cinquanta mondi usa e getta

Un caso che clicca su una card ha bisogno di una card da cliccare. Niente viene iniettato nel processo in esecuzione, quindi lo stato deve esistere prima che l'app parta. Ogni caso scrive una nuova directory di archiviazione nella cartella temporanea di sistema, ci semina dentro una board, e poi avvia l'app puntata su di essa:

app.launchArguments = [
    "-meatpad.storageRootOverride", storageRoot.path,
    "-meatpad.revealBoard", boardID.uuidString,
    "-hasSeenFirstRunIntro", "YES",
]

Quei tre flag sono codice reale nel binario di release. Esistono perché un test possa raggiungere una schermata, e vengono distribuiti a tutti. Questo è il costo onesto di testare un'interfaccia nativa dall'esterno, e lo pagheremmo di nuovo, ma va detto ad alta voce invece che scoperto da qualcuno che legge il codice sorgente. Il lato positivo è che l'esecuzione non lascia nulla dietro di sé: ogni caso elimina la propria cartella nel teardown, quindi le tue board non vengono mai toccate, e un fallimento non può contaminare il caso successivo.

Inizia da una nota. Resta per l’editor. MeatPad è un blocco note e un editor di codice nativi per macOS: file semplici sul tuo Mac, nessun account, nessun motore di sincronizzazione, nessuna telemetria.

Scopri MeatPad

L'orologio, suite per 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

Dodici suite, e ciascuna prende il nome da ciò che protegge: le tre modalità di visualizzazione delle card, l'editor delle card con i suoi campioni di colore e le scadenze, la modifica del titolo sul posto e il trascinamento di una card tra colonne, i link dentro le note delle card, le zone di rilascio, le etichette e il loro filtraggio, gli a capo nel campo di aggiunta rapida, la ricerca, il focus da tastiera del foglio per la nuova board, il suggerimento di link dell'editor di note, e i sei casi che chiedono a Launch Services se MeatPad viene offerto per ogni tipo di file che dichiara.

BoardLabel è quella costosa, con 102.638 secondi per 8 casi, perché il filtraggio per etichetta rimuove le card dalla vista uno o due frame dopo la pressione del tasto, quindi quei casi restano in un ciclo di polling in attesa che la colonna si assesti. Il singolo caso più lento della sessione è stato 27.992 secondi, per verificare che un titolo di card lungo venga tagliato nella visualizzazione compatta e vada a capo nelle altre due, il che significa misurare lo stesso titolo tre volte in tre avvii. Il caso più veloce è stato 1.507 secondi, chiedendo a Launch Services se MeatPad viene offerto per ogni tipo di file che registra, cosa che richiede l'app in esecuzione ma non tocca mai la sua finestra.

La riga di riepilogo che ti mentirà

Il progetto Xcode di MeatPad viene generato da un manifest e non viene incluso nel commit. Questo è comodo fino al giorno in cui aggiungi un file di test, dimentichi di rigenerare, ed esegui comunque la suite. Dal punto di vista di xcodebuild non c'è niente che non va. Compila il progetto che gli è stato dato, esegue gli zero test al suo interno, e stampa le due parole che tutti cercano:

** TEST SUCCEEDED **

Una sessione che non ha eseguito niente sembra esattamente identica a una sessione che ha eseguito tutto. Quindi la riga che vale la pena leggere non è mai l'ultima. È il conteggio sopra di essa, che dice quanti casi sono realmente avvenuti. Ecco le ultime righe della sessione nella registrazione qui sopra. Il terminale nel video incanala xcodebuild attraverso un grep e un sed, quindi stampa una forma abbreviata dello stesso output e le righe entrano accanto alla finestra dell'app. Il comando stesso è invariato:

[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 **

Cinquanta casi, uno saltato apposta, nessun fallimento, 405.032 secondi. Il conteggio e l'ultima riga concordano, che è l'unica combinazione degna di fiducia.

Cosa è successo prima della ripresa verde

L'esecuzione registrata non era la prima della giornata. Una non registrata alle 12:19 è andata in rosso: gli stessi 50 casi, 1 saltato, e 2 fallimenti, entrambi in OpenWith, il caso della selezione multipla e il caso del secondo file, gli stessi due che vedi passare a 3.799 e 4.066 secondi nell'estratto sopra. I crash report dicono che l'app si è interrotta 0.75 secondi dopo l'avvio, dentro il percorso di avvio di SwiftUI, per un fallimento di precondizione di AttributeGraph sollevato da applicationWillFinishLaunching mentre il Dock stava ancora notificando l'app. Non le era stato ancora consegnato nulla, quindi non è il codice di apertura file. Quegli stessi 6 casi OpenWith, eseguiti da soli subito dopo, sono passati 3 volte su 3, cioè 18 casi su 18, e tutti e 6 sono passati di nuovo nell'esecuzione registrata. Sembra un difetto intermittente di tempistica all'avvio che appare solo quando la suite riavvia l'app una dopo l'altra. È ancora sotto indagine, e non viene tagliato via da nulla.

Una cosa scoperta durante l'allestimento merita un paragrafo tutto suo. Per far stare l'app accanto al terminale, prima abbiamo ridotto la finestra salvata di MeatPad da 1440x900 punti a 1010x900. Quattro casi sono falliti in un colpo solo: i campioni di colore nell'editor della card, un filtro per etichetta e una ricerca, tutti puramente perché l'elemento che cercavano di raggiungere era fuori schermo. Rieseguiti alla dimensione originale di 1440x900, lo stesso gruppo è passato 9 su 9. La dimensione della finestra è un input per un test di interfaccia, e questa suite assume silenziosamente la dimensione predefinita dell'app. Ecco perché la registrazione mostra MeatPad alla sua dimensione predefinita a destra e un terminale stretto a sinistra.

Due riprese precedenti sono finite nel cestino per motivi che non avevano nulla a che fare con MeatPad: in una, una finestra di crash rimasta a schermo dall'esecuzione delle 12:19, e nell'altra, la suite di MailVault di un altro job che metteva le proprie finestre sopra a metà corsa, perché il mini è una macchina condivisa. Il che è una discreta introduzione alla seconda metà del pomeriggio.

Atto secondo: MailVault, 81 file di spec in sedici minuti

Guarda su YouTube

MailVault 2.11.3 è costruito nel modo opposto: un core Rust con un front end web, impacchettato da Tauri. La sua suite end-to-end è guidata da WebdriverIO, e il comando delle 14:43 ha richiesto due delle sue tre suite:

npx wdio run wdio.conf.js --suite ui-headless --suite connected-ci

La suite ui-headless è composta da 7 file di spec che coprono lo stato di benvenuto senza nessun account configurato. La suite connected-ci è composta da 74 file di spec che girano contro account IMAP fittizi preseminati. Sono 81 file di spec in totale. Una terza suite, local-manual, contiene altri 6 file di spec per backup, migrazione, archivio e controlli visivi, e non faceva parte di questa esecuzione.

Spec Files:      80 passed, 1 failed, 81 total (100% completed) in 00:16:03

Ottanta file di spec passati, uno fallito, 81 in totale, in 16 minuti e 3 secondi secondo l'orologio di wdio. Il video dura 16:18 perché inizia prima del comando e finisce dopo. Sotto quei file ci sono 539 casi di test: 533 passati, 5 saltati e il fallimento unico. La metà ui-headless ha contribuito con 55 passati e 3 saltati in circa 56 secondi di tempo di test, e connected-ci ha contribuito con 478 passati, 1 fallito e 2 saltati in circa 811 secondi.

L'app viene avviata una volta per file di spec, non una volta per caso, quindi i 539 casi di MailVault del pomeriggio costano 81 avvii. Un file di spec dura in media circa 10.7 secondi e la mediana è 6 secondi, il che dice che la media è trascinata da una manciata di file lunghi: connected-custody-claims con 54.3 secondi per 9 test, connected-performance con 53.4 secondi per 7, connected-compose-editor con 39.6 secondi per 19, connected-compose-autosave con 38.7 secondi per 15. Il file più veloce, connected-backup-partial-failure, ha eseguito i suoi 5 test in 32 millisecondi. Il più affollato, connected-email-viewer, ne contiene 21. Tutto questo poggia su una base molto più grande e molto più economica: 2,683 test unitari vitest in 216 file finiscono in 6.77 secondi, 318 test Rust coprono il core, e le due suite registrate sono 518 casi it() nel codice sorgente.

Due cose nella registrazione meritano una parola. Il terminale a sinistra stampa solo le righe RUNNING, PASSED e FAILED di wdio per ogni file, perché wdio riserva il suo report dettagliato per la fine stessa dell'esecuzione. E verso la fine, la spec di esportazione apre un messaggio esportato in Preview, che copre il terminale per l'ultimo minuto; è il test che fa il suo lavoro, non un incidente.

Cosa prepara l'imbracatura di test

Ogni esecuzione avvia i propri server IMAP fittizi con tre account preseminati, luke, vader e yoda su mock.test, e la INBOX di vader contiene 700 messaggi, comodamente più delle finestre di paginazione dell'app, così la paginazione viene messa alla prova invece che data per scontata. Un quarto account viene aggiunto da un test che puoi vedere digitato dal vivo. L'app sotto test è una build con la funzionalità webdriver attivata, avviata da tauri-wd, il crate tauri-webdriver-automation, e gira contro una directory HOME usa e getta così un vault reale non viene mai toccato. Quell'imbracatura di test ha il suo approfondimento in Nota sul campo 004.

Il fallimento unico, non classificato

Un caso su 539 è fallito: la spec connected-storage-matrix, test "a row deleted in unified mode stays gone across account churn and a reload". Il suo helper switchToUnified verifica immediatamente che il pulsante Tutte le caselle della barra laterale esista e sia visibile, senza nessuna attesa, e gira subito dopo che il test precedente ha ricaricato l'app. Atteso true, ricevuto false. Non siamo riusciti a rieseguire quella singola spec in seguito, perché ogni tentativo di avviare una nuova sessione driver in quel checkout falliva prima di raggiungere il driver, con un undici UND_ERR_INVALID_ARG sulla richiesta di sessione. Quindi resta non classificato. Sembra un'attesa mancante dopo un ricaricamento più che un bug di prodotto, e resta nel video.

1.8 secondi a caso contro 8.3

539 casi di MailVault in 963 secondi di tempo reale fanno circa 1.8 secondi a caso. I 49 casi eseguiti di MeatPad in 405.032 secondi fanno circa 8.3 secondi ciascuno. Stessa macchina, stesso pomeriggio, un fattore di circa cinque tra i due.

Due ragioni strutturali, non un'ottimizzazione furba. WebDriver esegue JavaScript dentro l'applicazione in esecuzione e legge il DOM direttamente, quindi un controllo si risolve contro uno dei 494 hook data-testid nelle spec end-to-end invece di attraversare un confine di processo verso l'albero di accessibilità di macOS. E l'aritmetica degli avvii è diversa: 539 casi di MailVault costano 81 avvii dell'app, mentre 50 casi di MeatPad ne costano 50. Abbiamo misurato bene quel divario un paio di settimane fa in Nota sul campo 006.

Niente di tutto questo rende un'app testata meglio dell'altra. Le suite controllano prodotti diversi, e 8.3 secondi a caso sono il prezzo di guidare un'interfaccia nativa nell'unico modo che macOS offre dall'esterno del processo.

Eseguili tu stesso

git clone https://github.com/GraphicMeat/MeatPad
cd MeatPad
brew install xcodegen
xcodegen generate
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
  -only-testing:MeatPadUITests

Non saltare la riga xcodegen, per il motivo detto sopra. Il codice sorgente di MeatPad è su GitHub. La suite di MailVault è un altro semplice clone e lo stesso comando eseguito davanti alla telecamera:

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

È tutto pubblico, incluso il difetto intermittente di MeatPad e il fallimento unico di MailVault, e il codice sorgente di MailVault sta accanto a quello di MeatPad su GitHub.

Software per Mac local-first, con le misurazioni allegate. MailVault tiene la tua posta in un vault sul tuo disco, e una macchina clicca su tutta l'app prima di ogni release.

Scopri MailVault