Bajo el escritorio hay un Mac mini con Apple M4 y 16 GB de memoria, y su trabajo es averiguar si nuestras aplicaciones siguen funcionando. MailVault está construido con Tauri. MeatPad está construido con SwiftUI. El mini ejecuta ambas suites: misma máquina, misma tarde, misma persona escribiendo las pruebas.
Las dos suites no cuestan lo mismo. No cuestan nada parecido. Todo lo que sigue se midió el 25 de agosto de 2026, y cada comando es uno que puedes ejecutar tú mismo.
Lo que salió en verde hoy
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
Lee las dos últimas líneas de cada bloque una contra otra, porque ahí está todo el artículo. MailVault obtiene 1.440 comprobaciones sin ventana en menos de siete segundos de reloj, y además puede conducir su propio binario real a través de 53 specs de extremo a extremo. MeatPad obtiene 601 comprobaciones sin ventana en once segundos, y 31 pruebas que tocan la interfaz real.
Luego está el número que no cabe en una tabla. Ejecutar ocho de esas 31 pruebas de interfaz en el mini tardó 98,186 segundos. Los casos individuales fueron de 6,9 a 17,2 segundos. Llamémoslo doce segundos por prueba.
Executed 8 tests, with 0 failures (0 unexpected) in 98.186 seconds
Una prueba unitaria de MeatPadKit en la misma máquina promedia diecinueve milisegundos. Un caso de vitest de MailVault promedia menos de dos. Un solo clic simulado sobre una vista de SwiftUI cuesta unas seiscientas pruebas unitarias.
Agosto, en dos curvas
MailVault pasó el mes recibiendo pruebas, y la forma de eso se ve fácilmente contando los casos de prueba del repositorio semana a semana:
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
El número de casos casi se duplicó en veinticinco días, y las specs de extremo a extremo más que se duplicaron. Ese crecimiento no está repartido de forma uniforme por la aplicación. Fue adonde estaban los fallos: redacción y borradores, selección múltiple, la matriz de almacenamiento que tiene que coincidir sobre lo que hay en el disco después de cada acción que modifica algo, y el cambio de cuenta. Los títulos de los commits de ese tramo se leen como una lista de confesiones, que es como debe leerse un registro de pruebas.
La curva de MeatPad es más corta, porque la aplicación es más joven, y tiene un escalón sobre el que conviene ser honesto:
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 salió en julio con 517 pruebas y ni una sola abría una ventana. La primera prueba de interfaz se escribió el 24 de agosto, un mes después del lanzamiento. Ese hueco no es pereza. Es el tema del resto de este artículo.
Un archivo de correo que puedes comprobar de verdad. MailVault respalda y archiva tu buzón en local, y 53 specs de extremo a extremo conducen la aplicación real antes de que nada de eso llegue hasta ti.
Visita MailVault →La diferencia que explica todas las demás
La interfaz de una app de Tauri es una página web. El arnés de pruebas le habla en WebDriver, lo que significa que el arnés puede ejecutar código dentro de la propia aplicación en marcha:
const searchExists = await browser.execute(() => {
const input = document.querySelector('input[placeholder*="Search"]');
return input !== null;
});
Esa función flecha no la evalúa la prueba. Se envía a la webview de la propia aplicación, se ejecuta allí contra el documento vivo, y su resultado vuelve. La prueba puede interrogar la interfaz exactamente igual que puede hacerlo la aplicación misma.
XCUITest no puede hacer esto y nunca podrá. Es un proceso aparte que conduce tu aplicación desde fuera a través de la capa de accesibilidad de macOS. No hay canal para código arbitrario, ni forma de hacerle a una vista una pregunta que el árbol de accesibilidad no responda ya. Todo lo que sigue es consecuencia de esa única frontera.
En un documento, todo tiene asa
La suite de extremo a extremo de MailVault selecciona elementos 305 veces mediante atributos de identificador de prueba. Añadir uno no cuesta nada y no cambia nada en cómo funciona la aplicación. Todo lo que la aplicación dibuja es direccionable, lo hubiera planeado alguien o no.
MeatPad tiene 34 identificadores de accesibilidad en todo su árbol de fuentes, y cada uno tuvo que colgarse deliberadamente de una vista dentro del código de producción que se publica:
TextField("Filter", text: $query)
.accessibilityIdentifier("board.labelFilter")
Todo lo que no tenga uno, a efectos de pruebas, no está. La superficie de prueba de una app de SwiftUI no es «la interfaz». Es «las partes de la interfaz que alguien se acordó de nombrar», y esa lista solo crece cuando se está escribiendo una prueba.
Y no puedes preguntar qué clase de cosa nombraste
Incluso con un identificador, una vista de SwiftUI no te promete en qué se convirtió. Así llegan las pruebas de MeatPad al campo de búsqueda del tablero:
app.descendants(matching: .any).matching(identifier: "board.search").firstMatch
Buscar por cualquier tipo de descendiente es una rendición. En el código fuente pone TextField, pero que aparezca como campo de texto, campo de búsqueda o algo completamente distinto depende de cómo decidiera SwiftUI construirlo ese día, y eso puede cambiar con una actualización del sistema. Recorrer todos los tipos de elemento es más lento y más vago que pedir aquello que escribiste, y es la única opción fiable.
Volver a leer el valor es la misma historia. El título de una tarjeta vuelve como una conversión opcional sobre un valor de tipo desconocido, porque eso es todo lo que ofrece la capa de accesibilidad, y acertar al adivinar es tu trabajo.
Nadie te espera
WebDriver trae la espera incorporada. XCUITest espera a que exista un elemento y no ofrece absolutamente nada para una condición que abarque varios. Así que las pruebas de MeatPad se la fabrican 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
}
Las tarjetas abandonan la jerarquía de vistas uno o dos fotogramas después de la pulsación que las filtró, así que una sola aserción justo después de escribir es cara o cruz. Cada prueba que comprueba una lista tras una interacción necesita este bucle, y cada uno de esos bucles es un sitio donde un fallo real puede pasarse diez segundos pareciendo un éxito lento.
Poner la aplicación en un estado conocido
La suite de MailVault arranca un servidor IMAP simulado por cuenta con una bandeja de entrada preparada de 700 mensajes, apunta la aplicación hacia ellos, y le da a toda la ejecución un directorio personal desechable. La aplicación bajo prueba está completamente sin modificar. No tiene ni idea de que la están probando.
A MeatPad hay que decírselo. Sus pruebas de interfaz escriben un directorio de almacenamiento en el disco y luego lanzan la aplicación con argumentos que el binario publicado entiende:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Esos indicadores son código real en la aplicación publicada, y existen para que las pruebas puedan alcanzar una pantalla. Es un intercambio justo y lo volveríamos a hacer, pero conviene nombrarlo: con XCUITest, parte de tu arnés de pruebas se entrega a los usuarios.
Dónde se permite que corran las pruebas
La suite de extremo a extremo sin ventana de MailVault corre en un runner de GitHub Actions con Ubuntu corriente. El binario real de Tauri se compila para Linux, se lanza dentro de un búfer de imagen virtual y una sesión de bus de mensajes, y WebDriver lo va pulsando sobre hardware que no cuesta nada por minuto. El conjuro exacto está más abajo.
dbus-run-session -- xvfb-run --auto-servernum \
--server-args="-screen 0 1920x1080x24" \
npx wdio run wdio.conf.js --suite ui-headless
No hay línea equivalente para XCUITest. Necesita macOS, un servidor de ventanas y una sesión de usuario real, porque de verdad está moviendo un puntero y pulsando teclas. El repositorio de MeatPad tiene exactamente un flujo de trabajo de GitHub y compila versiones. Las pruebas viven en el mini, que es la respuesta honesta a «dónde ejecuta un estudio pequeño las pruebas de interfaz de Mac», y la razón de que esa máquina exista. Contamos cómo se gana el sueldo con MailVault en una nota anterior.
La parte de MeatPad que ninguna prueba de interfaz alcanzará jamás
Mira otra vez dónde están las 31 pruebas de interfaz de MeatPad: presentación de tarjetas, editor de tarjeta, etiquetas, saltos de línea, búsqueda. Todo eso es el tablero. Ni una sola toca el editor, que es el producto de verdad.
El editor envuelve STTextView sobre TextKit 2 dentro de un NSViewRepresentable, y para el árbol de accesibilidad es una única región opaca que contiene texto. Cursores múltiples, marcadores de fragmento, plegado de código, resaltado incremental con tree-sitter, emparejado de paréntesis: nada de eso existe como elementos que consultar. No puedes afirmar que el segundo cursor cayó en la línea cuarenta, porque no hay ningún elemento llamado «el segundo cursor».
Así que ese trabajo no se prueba a través de la interfaz. Se prueba por debajo. MeatPadKit es un paquete de Swift aparte, 47 archivos fuente, que contiene el escáner de plegado, el modelo multicursor, el analizador de fragmentos, el resaltador, el comparador difuso, el puente de posiciones de LSP y el índice de símbolos del proyecto. No tiene código de vista, y precisamente por eso 601 pruebas pueden correr contra él en once segundos sin ninguna ventana en pantalla.
El comentario al principio de una de las pruebas de interfaz de MeatPad expresa la división mejor de lo que podríamos parafrasear: MeatPadKit prueba unitariamente el propio comparador, y lo que no puede alcanzar es si el campo está siquiera conectado a las columnas. Esa es la descripción completa del puesto de una prueba de interfaz de SwiftUI. No «funciona la lógica», que ya respondió una prueba rápida sin ventana, sino «está enchufado», que no lo puede responder ninguna otra cosa.
Empieza con una nota. Quédate por el editor. MeatPad es un cuaderno de notas y editor de código nativo de macOS: archivos simples en tu Mac, sin cuenta, sin motor de sincronización, sin telemetría.
Ver MeatPad →Lo que esto cambia de verdad en tu forma de construir
La lección no es que SwiftUI sea malo. Es que el coste de verificar una interfaz nativa es lo bastante alto como para ser una entrada de arquitectura y no una ocurrencia tardía.
- Saca la lógica de la vista hasta que la vista sea aburrida. MeatPad son 51 archivos fuente de interfaz y 47 de framework, y el lado del framework carga con el 95 por ciento de las pruebas. Todo lo que quede en una vista de SwiftUI es caro de comprobar, así que la cantidad correcta que dejar ahí es la maquetación.
- Gasta las pruebas de interfaz en el cableado, no en el comportamiento. A doce segundos cada una son la herramienta equivocada para los casos límite y la correcta para «el campo de búsqueda está conectado a la columna».
- Añade los identificadores cuando escribas la vista, no cuando escribas la prueba. Ponerlos después significa editar archivos de producción para hacer posible una prueba, que es justo el momento en que la gente deja de escribir pruebas.
- Si tu interfaz es una webview, acepta la comida gratis. Poder ejecutar una consulta dentro de tu propia aplicación en marcha es una ventaja auténtica de la pila de Tauri, y por eso MailVault pudo añadir 31 specs de extremo a extremo en un solo mes.
Lo que no está cubierto
Dos huecos reconocidos, porque un artículo sobre pruebas que solo cuenta las victorias es un folleto.
El editor de MeatPad no tiene ninguna cobertura a nivel de interfaz. La lógica que hay debajo está probada a fondo, y el pegamento entre esa lógica y la vista de texto lo revisa una persona. Una regresión ahí llegaría a una versión publicada. Ir royendo eso con un número pequeño de pruebas caras, en las interacciones que valen doce segundos cada una, es lo siguiente.
Del lado de MailVault, solo la suite sin ventana corre en integración continua. Las 41 specs preparadas y las 5 locales corren en el mini, a demanda, lo que significa que atrapan cosas antes de una publicación en lugar de antes de una fusión. Y una suite en verde sigue demostrando solo las cosas que alguien pensó en comprobar, y por eso el número de esa tabla importa menos que el hecho de que siga moviéndose.
Reproduce cualquiera de estos datos
# 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
El último comando es el que tarda dos minutos. Ahora sabes por qué.
Dos aplicaciones, una máquina que las mantiene honestas. Software para Mac local-first, de un estudio que publica sus mediciones, incluidas las que no le favorecen.
Ver lo que construimos →