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