Sotto la scrivania c'è un Mac mini con Apple M4 e 16 GB di memoria, e il suo compito è scoprire se le nostre app funzionano ancora. MailVault è costruito con Tauri. MeatPad è costruito con SwiftUI. Il mini esegue entrambe le suite: stessa macchina, stesso pomeriggio, stessa persona che scrive i test.
Le due suite non costano lo stesso. Non costano niente di simile. Tutto quello che segue è stato misurato il 25 agosto 2026, e ogni comando è un comando che puoi eseguire tu stesso.
Cosa è passato in verde oggi
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
Leggi le ultime due righe di ciascun blocco l'una contro l'altra, perché lì c'è tutto l'articolo. MailVault ottiene 1.440 verifiche senza finestra in meno di sette secondi di tempo reale, e in più può guidare il proprio binario reale attraverso 53 spec end-to-end. MeatPad ottiene 601 verifiche senza finestra in undici secondi, e 31 test che toccano l'interfaccia vera.
Poi c'è il numero che non sta in una tabella. Eseguire otto di quei 31 test di interfaccia sul mini ha richiesto 98,186 secondi. I singoli casi sono andati da 6,9 a 17,2 secondi. Chiamiamoli dodici secondi per test.
Executed 8 tests, with 0 failures (0 unexpected) in 98.186 seconds
Un test unitario di MeatPadKit sulla stessa macchina fa in media diciannove millisecondi. Un caso vitest di MailVault fa in media meno di due. Un solo clic simulato su una vista SwiftUI costa circa seicento test unitari.
Agosto, in due curve
MailVault ha passato il mese a farsi scrivere i test, e la forma di tutto questo si vede facilmente contando i casi di test nel repository settimana per settimana:
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
Il numero di casi è quasi raddoppiato in venticinque giorni, e le spec end-to-end sono più che raddoppiate. Quella crescita non è distribuita in modo uniforme nell'app. È andata dove erano i bug: composizione e bozze, selezione multipla, la matrice di archiviazione che deve trovarsi d'accordo su cosa c'è sul disco dopo ogni azione che modifica qualcosa, e il cambio di account. I titoli dei commit di quel tratto si leggono come un elenco di confessioni, che è il modo giusto in cui deve leggersi un registro di test.
La curva di MeatPad è più corta, perché l'app è più giovane, e ha un gradino su cui vale la pena essere onesti:
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 è uscito a luglio con 517 test e nemmeno uno di essi apriva una finestra. Il primo test di interfaccia è stato scritto il 24 agosto, un mese dopo il rilascio. Quel divario non è pigrizia. È l'argomento del resto di questo articolo.
Un archivio di posta che puoi davvero controllare. MailVault esegue il backup e archivia la tua casella in locale, e 53 spec end-to-end guidano l'applicazione reale prima che qualcosa di tutto ciò arrivi a te.
Visita MailVault →La differenza che spiega tutte le altre
L'interfaccia di un'app Tauri è una pagina web. L'imbracatura di test le parla in WebDriver, il che significa che l'imbracatura può eseguire codice dentro l'applicazione in esecuzione stessa:
const searchExists = await browser.execute(() => {
const input = document.querySelector('input[placeholder*="Search"]');
return input !== null;
});
Quella funzione freccia non viene valutata dal test. Viene spedita dentro la webview dell'app, eseguita lì contro il documento vivo, e il suo risultato torna indietro. Il test può interrogare l'interfaccia esattamente come può farlo l'applicazione stessa.
XCUITest non può farlo e non potrà mai. È un processo separato che guida la tua app dall'esterno attraverso il livello di accessibilità di macOS. Non c'è alcun canale per codice arbitrario, nessun modo di porre a una vista una domanda a cui l'albero di accessibilità non risponda già. Tutto ciò che segue è una conseguenza di quell'unico confine.
In un documento, tutto ha una maniglia
La suite end-to-end di MailVault seleziona elementi 305 volte tramite attributi di identificatore di test. Aggiungerne uno non costa nulla e non cambia nulla nel modo in cui l'app funziona. Tutto ciò che l'app disegna è indirizzabile, che qualcuno lo avesse previsto o no.
MeatPad ha 34 identificatori di accessibilità in tutto il suo albero sorgente, e ognuno di essi è stato attaccato deliberatamente a una vista nel codice di produzione che viene rilasciato:
TextField("Filter", text: $query)
.accessibilityIdentifier("board.labelFilter")
Tutto ciò che non ne ha uno, ai fini dei test, non c'è. La superficie di test di un'app SwiftUI non è «l'interfaccia». È «le parti dell'interfaccia che qualcuno si è ricordato di nominare», e quell'elenco cresce solo mentre si sta scrivendo un test.
E non puoi chiedere che tipo di cosa hai nominato
Anche con un identificatore, una vista SwiftUI non ti promette che cosa è diventata. Ecco come i test di MeatPad raggiungono il campo di ricerca della board:
app.descendants(matching: .any).matching(identifier: "board.search").firstMatch
Cercare su un tipo qualsiasi di discendente è una resa. Nel sorgente c'è scritto TextField, ma se emerga come campo di testo, campo di ricerca o qualcosa di completamente diverso dipende da come SwiftUI ha scelto di costruirlo quel giorno, e questo può cambiare con un aggiornamento del sistema. Percorrere ogni tipo di elemento è più lento e più vago che chiedere la cosa che hai scritto, ed è l'unica opzione affidabile.
Rileggere il valore è la stessa storia. Il titolo di una card torna come un cast opzionale su un valore di tipo sconosciuto, perché è tutto ciò che il livello di accessibilità offre, e indovinare giusto è compito tuo.
Nessuno ti aspetta
WebDriver ha l'attesa integrata. XCUITest attende che un elemento esista e non offre assolutamente nulla per una condizione che ne coinvolga più di uno. Così i test di MeatPad se la costruiscono a mano:
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
}
Le card lasciano la gerarchia delle viste uno o due fotogrammi dopo il tasto che le ha filtrate via, quindi una singola asserzione subito dopo la digitazione è testa o croce. Ogni test che controlla un elenco dopo un'interazione ha bisogno di questo ciclo, e ognuno di quei cicli è un punto in cui un guasto vero può passare dieci secondi ad assomigliare a un successo lento.
Portare l'app in uno stato noto
La suite di MailVault avvia un server IMAP simulato per ogni account con una casella preparata di 700 messaggi, punta l'app su di essi, e dà all'intera esecuzione una home directory usa e getta. L'applicazione sotto test è del tutto immodificata. Non ha idea di essere testata.
A MeatPad bisogna dirlo. I suoi test di interfaccia scrivono una directory di archiviazione sul disco, poi lanciano l'app con argomenti che il binario rilasciato comprende:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Quei flag sono codice vero nell'applicazione pubblicata, ed esistono perché i test possano raggiungere una schermata. È uno scambio equo e lo rifaremmo, ma vale la pena dirlo chiaramente: con XCUITest, una parte della tua imbracatura di test viene consegnata agli utenti.
Dove i test hanno il permesso di girare
La suite end-to-end senza finestra di MailVault gira su un normale runner Ubuntu di GitHub Actions. Il binario Tauri vero viene compilato per Linux, lanciato dentro un framebuffer virtuale e una sessione di bus di messaggi, e cliccato da WebDriver su hardware che non costa nulla al minuto. La formula esatta è qui sotto.
dbus-run-session -- xvfb-run --auto-servernum \
--server-args="-screen 0 1920x1080x24" \
npx wdio run wdio.conf.js --suite ui-headless
Per XCUITest non esiste una riga equivalente. Gli servono macOS, un window server e una vera sessione utente, perché sta davvero muovendo un puntatore e premendo tasti. Il repository di MeatPad ha esattamente un workflow GitHub e costruisce i rilasci. I test vivono sul mini, che è la risposta onesta a «dove esegue un piccolo studio i test di interfaccia per Mac», e la ragione per cui quella macchina esiste. Abbiamo raccontato come si guadagna il posto per MailVault in una nota precedente.
La parte di MeatPad che nessun test di interfaccia raggiungerà mai
Guarda di nuovo dove sono i 31 test di interfaccia di MeatPad: visualizzazione delle card, editor della card, etichette, ritorni a capo, ricerca. Tutto questo è la board. Nemmeno uno tocca l'editor, che è il prodotto vero.
L'editor avvolge STTextView su TextKit 2 dentro un NSViewRepresentable, e per l'albero di accessibilità è un'unica regione opaca che contiene testo. Cursori multipli, segnaposto degli snippet, ripiegamento del codice, evidenziazione incrementale con tree-sitter, corrispondenza delle parentesi: niente di tutto ciò esiste come elementi da interrogare. Non puoi asserire che il secondo cursore è finito alla riga quaranta, perché non esiste alcun elemento chiamato «il secondo cursore».
Quindi quel lavoro non viene testato attraverso l'interfaccia. Viene testato sotto di essa. MeatPadKit è un pacchetto Swift separato, 47 file sorgente, che contiene lo scanner del ripiegamento, il modello multicursore, il parser degli snippet, l'evidenziatore, il comparatore fuzzy, il ponte delle posizioni LSP e l'indice dei simboli del progetto. Non contiene codice di vista, ed è esattamente per questo che 601 test possono girarci contro in undici secondi senza alcuna finestra sullo schermo.
Il commento in cima a uno dei test di interfaccia di MeatPad esprime la divisione meglio di quanto sapremmo parafrasare: MeatPadKit testa in modo unitario il comparatore stesso, e ciò che non riesce a raggiungere è se il campo sia in qualche modo collegato alle colonne. Questa è l'intera descrizione del ruolo di un test di interfaccia SwiftUI. Non «la logica funziona», a cui un test veloce senza finestra ha già risposto, ma «è collegato», a cui nient'altro può rispondere.
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 →Cosa cambia davvero nel modo in cui costruisci
La lezione non è che SwiftUI sia brutto. È che il costo di verificare un'interfaccia nativa è abbastanza alto da essere un dato architetturale in ingresso anziché un ripensamento.
- Spingi la logica fuori dalla vista finché la vista non è noiosa. MeatPad è 51 file sorgente di interfaccia e 47 di framework, e il lato framework porta il 95 per cento dei test. Tutto ciò che resta in una vista SwiftUI è costoso da controllare, quindi la quantità giusta da lasciarci è l'impaginazione.
- Spendi i test di interfaccia sul collegamento, non sul comportamento. A dodici secondi l'uno sono lo strumento sbagliato per i casi limite e quello giusto per «il campo di ricerca è collegato alla colonna».
- Aggiungi gli identificatori quando scrivi la vista, non quando scrivi il test. Metterli dopo significa modificare file di produzione per rendere possibile un test, che è esattamente il momento in cui le persone smettono di scrivere test.
- Se la tua interfaccia è una webview, prenditi il pranzo gratis. Poter eseguire una query dentro la tua stessa applicazione in esecuzione è un vantaggio autentico dello stack Tauri, ed è per questo che MailVault ha potuto aggiungere 31 spec end-to-end in un solo mese.
Cosa non è coperto
Due lacune dichiarate, perché un articolo sui test che riporta solo le vittorie è un dépliant.
L'editor di MeatPad non ha alcuna copertura a livello di interfaccia. La logica sottostante è testata a fondo, e la colla tra quella logica e la vista di testo viene controllata da una persona. Una regressione lì arriverebbe a un rilascio. Rosicchiare quel problema con un piccolo numero di test costosi, sulle interazioni che valgono dodici secondi ciascuna, è il prossimo passo.
Dal lato di MailVault, solo la suite senza finestra gira in integrazione continua. Le 41 spec preparate e le 5 locali girano sul mini, su richiesta, il che significa che intercettano le cose prima di un rilascio anziché prima di un merge. E una suite verde dimostra comunque solo le cose che qualcuno ha pensato di controllare, motivo per cui il numero in quella tabella conta meno del fatto che continui a muoversi.
Riprodurre qualunque di questi dati
# 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
L'ultimo comando è quello che impiega due minuti. Ora sai perché.
Due app, una macchina che le tiene oneste. Software per Mac local-first, da uno studio che pubblica le proprie misure, comprese quelle poco lusinghiere.
Guarda cosa costruiamo →