Hay un Apple M4 Mac mini con 16 GB de memoria en un estante, corriendo macOS 15, Xcode 26.6 y Swift 6.3.3, accesible solo por SSH en la red local. El 5 de septiembre de 2026 pasó una tarde ejecutando las pruebas de interfaz de nuestras dos apps de Mac, una tras otra, con la grabación de pantalla activada. MeatPad, nuestro cuaderno de notas y editor de código de macOS, es una app de SwiftUI probada con XCUITest: 50 casos, empezando a las 12:58. MailVault, nuestro archivador de correo local-first, es una app de Tauri probada con WebdriverIO: 539 casos, empezando a las 14:43. Son 589 casos de prueba en cámara, en una sola máquina, en una sola tarde, y las dos ejecuciones no se comportan nada parecido.
Acto uno: MeatPad, cincuenta lanzamientos en 405 segundos
Un comando, a la izquierda de la pantalla, contra MeatPad 0.15.0:
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
-only-testing:MeatPadUITests
Todo lo demás en esa grabación es consecuencia de esa línea. Xcode compila la app y un segundo paquete llamado test runner, y le entrega ambos a la máquina. Para cada caso de prueba, el runner arranca la app desde cero, espera su ventana, mueve el puntero, pulsa teclas, lee lo que volvió, y termina la app. Después lo repite para el siguiente caso. Las ventanas que aparecen y desaparecen no son un efecto visual. Eso es el bucle.
Por qué un caso de XCUITest cuesta segundos
La ejecución terminó 50 casos en 405.032 segundos. Uno de los 50 se omite a sí mismo a propósito, así que los 49 que sí corrieron promedian unos 8.3 segundos cada uno. Compara eso con MeatPadKit, el framework debajo de la interfaz, donde una prueba promedia diecinueve milisegundos.
Tres cosas explican esa diferencia. La app se lanza una vez por caso, así que cada caso paga un arranque en frío y la espera a que la ventana se asiente. La prueba corre en un proceso separado y solo puede ver la app a través del árbol de accesibilidad de macOS, así que cada pregunta sobre la pantalla es una consulta que cruza un límite de proceso en lugar de una lectura de memoria. Y el puntero y el teclado son reales, así que un arrastre tarda lo que tarda un arrastre.
Cincuenta mundos desechables
Un caso que hace clic en una tarjeta necesita una tarjeta que clicar. No se inyecta nada en el proceso en marcha, así que el estado tiene que existir antes de que arranque la app. Cada caso escribe un directorio de almacenamiento nuevo en la carpeta temporal del sistema, siembra un tablero en él, y luego lanza la app apuntada a él:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Esas tres opciones son código real en el binario de lanzamiento. Existen para que una prueba pueda alcanzar una pantalla, y se distribuyen a todo el mundo. Ese es el coste honesto de probar una interfaz nativa desde fuera de ella, y lo pagaríamos otra vez, pero conviene decirlo en voz alta en lugar de que alguien lo descubra leyendo el código fuente. La ventaja es que la ejecución no deja nada atrás: cada caso borra su directorio al terminar, así que tus propios tableros nunca se tocan, y un fallo no puede contaminar el siguiente caso.
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 →El reloj, suite por 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
Doce suites, y cada una lleva el nombre de lo que protege: los tres modos de visualización de tarjeta, el editor de tarjeta con sus muestras de color y fechas de vencimiento, la edición del título en el sitio y arrastrar una tarjeta entre columnas, los enlaces dentro de las notas de la tarjeta, los objetivos de destino, las etiquetas y su filtrado, los saltos de línea en el campo de añadido rápido, la búsqueda, el foco de teclado de la hoja de nuevo tablero, la pista de enlace del editor de notas, y los seis casos que le preguntan a Launch Services si MeatPad se ofrece para cada tipo de archivo que dice manejar.
BoardLabel es el caro, con 102.638 segundos para 8 casos, porque el filtrado por etiquetas quita tarjetas de la vista uno o dos fotogramas después de la pulsación, así que esos casos se quedan en un bucle de sondeo esperando a que la columna se asiente. El caso individual más lento de la ejecución fue de 27.992 segundos, comprobando que un título de tarjeta largo se recorta en la vista compacta y se envuelve en las otras dos, lo que significa medir el mismo título tres veces en tres lanzamientos. El caso más rápido fue de 1.507 segundos, preguntando a Launch Services si MeatPad se ofrece para cada tipo de archivo que registra, lo cual necesita la app corriendo pero nunca toca su ventana.
La línea de resumen que te va a mentir
El proyecto de Xcode de MeatPad se genera a partir de un manifiesto y no está en el repositorio. Eso es cómodo hasta el día que añades un archivo de pruebas, olvidas regenerar, y corres la suite de todos modos. Nada está mal desde el punto de vista de xcodebuild. Compila el proyecto que le dieron, corre las cero pruebas que hay dentro, e imprime las dos palabras que todo el mundo busca:
** TEST SUCCEEDED **
Una ejecución que no ejecutó nada se ve exactamente igual que una ejecución que ejecutó todo. Así que la línea que vale la pena leer nunca es la última. Es el recuento justo encima, que dice cuántos casos ocurrieron de verdad. Aquí están las últimas líneas de la ejecución de la grabación de arriba. La terminal del vídeo canaliza xcodebuild a través de un grep y un sed, así que imprime una forma abreviada de la misma salida y las líneas caben junto a la ventana de la app. El comando en sí no cambia:
[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 **
Cincuenta casos, uno omitido a propósito, ningún fallo, 405.032 segundos. El recuento y la última línea coinciden, que es la única combinación digna de confianza.
Qué pasó antes de la toma en verde
La ejecución grabada no fue la primera del día. Una sin grabar a las 12:19 salió en rojo: los mismos 50 casos, 1 omitido, y 2 fallos, ambos en OpenWith, el caso de selección múltiple y el caso del segundo archivo, los dos que se ven pasar a 3.799 y 4.066 segundos en el fragmento de arriba. Los informes de fallo dicen que la app abortó 0.75 segundos después del lanzamiento, dentro del propio camino de arranque de SwiftUI, por un fallo de precondición de AttributeGraph levantado desde applicationWillFinishLaunching mientras el Dock aún notificaba a la app. No se le había entregado nada todavía, así que esto no es el código de abrir archivos. Esos mismos 6 casos de OpenWith, corridos por su cuenta justo después, pasaron 3 de 3 veces, que son 18 casos de 18, y los 6 volvieron a pasar en la ejecución grabada. Parece un fallo intermitente de tiempos de arranque que solo aparece cuando la suite relanza la app una y otra vez seguidas. Sigue bajo investigación, y no se ha editado nada para ocultarlo.
Una cosa encontrada durante la preparación merece su propio párrafo. Para que la app cupiera junto a la terminal, primero encogimos el marco de ventana guardado de MeatPad de 1440x900 puntos a 1010x900. Cuatro casos fallaron de golpe: las muestras de color del editor de tarjeta, un filtro de etiqueta y una búsqueda, todos ellos únicamente porque el elemento que buscaban estaba fuera de pantalla. Corrido de nuevo con el marco original de 1440x900, el mismo conjunto pasó 9 de 9. El tamaño de la ventana es una entrada de una prueba de interfaz, y esta suite asume en silencio el marco predeterminado de la app. Por eso la grabación muestra MeatPad a su tamaño predeterminado a la derecha y una terminal estrecha a la izquierda.
Dos tomas anteriores fueron a la basura por razones que no tenían nada que ver con MeatPad: un diálogo de fallo obsoleto que quedó de la ejecución de las 12:19 plantado en la pantalla en una, y, en la otra, la suite de MailVault de otro trabajo poniendo sus propias ventanas por encima a mitad de camino, porque el mini es una máquina compartida. Lo cual es una buena introducción a la segunda mitad de la tarde.
Acto dos: MailVault, 81 archivos de specs en dieciséis minutos
MailVault 2.11.3 está construido de la otra forma: un núcleo en Rust con un frontend web, empaquetado por Tauri. Su suite de extremo a extremo la conduce WebdriverIO, y el comando de las 14:43 pidió dos de sus tres suites:
npx wdio run wdio.conf.js --suite ui-headless --suite connected-ci
La suite ui-headless son 7 archivos de specs que cubren el estado de bienvenida sin cuentas configuradas. La suite connected-ci son 74 archivos de specs que corren contra cuentas IMAP simuladas y sembradas. Eso son 81 archivos de specs. Una tercera suite, local-manual, guarda 6 archivos de specs más para copias de seguridad, migración, archivo y comprobaciones visuales, y no formó parte de esta ejecución.
Spec Files: 80 passed, 1 failed, 81 total (100% completed) in 00:16:03
Ochenta archivos de specs pasaron, uno falló, 81 en total, en 16 minutos y 3 segundos según el propio reloj de wdio. El vídeo dura 16:18 porque empieza antes del comando y termina después. Debajo de esos archivos hay 539 casos de prueba: 533 pasando, 5 omitidos y el único fallo. La mitad de ui-headless aportó 55 aprobados y 3 omitidos en unos 56 segundos de tiempo de prueba, y connected-ci aportó 478 aprobados, 1 fallido y 2 omitidos en unos 811 segundos.
La app se lanza una vez por archivo de specs en lugar de una vez por caso, así que los 539 casos de MailVault de la tarde cuestan 81 lanzamientos. Un archivo de specs dura de media unos 10.7 segundos y la mediana es 6 segundos, lo que dice que la media la cargan un puñado de archivos largos: connected-custody-claims con 54.3 segundos para 9 pruebas, connected-performance con 53.4 segundos para 7, connected-compose-editor con 39.6 segundos para 19, connected-compose-autosave con 38.7 segundos para 15. El archivo más rápido, connected-backup-partial-failure, corrió sus 5 pruebas en 32 milisegundos. El más cargado, connected-email-viewer, guarda 21 pruebas. Todo esto se apoya en una base mucho más grande y mucho más barata: 2,683 pruebas unitarias de vitest en 216 archivos terminan en 6.77 segundos, 318 pruebas de Rust cubren el núcleo, y las dos suites grabadas son 518 casos it() en el código fuente.
Dos cosas en la grabación merecen una aclaración. La terminal de la izquierda solo imprime las líneas RUNNING, PASSED y FAILED de wdio por archivo, porque wdio guarda su informe detallado para el final mismo de la ejecución. Y cerca del final, el spec de exportación abre un mensaje exportado en Preview, que cubre la terminal durante el último minuto; eso es la prueba haciendo su trabajo, no un accidente.
Lo que prepara el arnés
Cada ejecución arranca sus propios servidores IMAP simulados con tres cuentas sembradas, luke, vader y yoda en mock.test, y la INBOX de vader guarda 700 mensajes, bastante más que las ventanas de paginación de la app, así que la paginación se ejercita en lugar de darse por supuesta. Una cuarta cuenta se añade con una prueba que se puede ver escribiéndose. La app bajo prueba es una compilación con la función webdriver activada, arrancada por tauri-wd, el crate tauri-webdriver-automation, y corre contra un directorio HOME desechable, así que un vault real nunca se toca. Ese arnés tiene su propio artículo en Nota de campo 004.
El único fallo, sin clasificar
Un caso de los 539 falló: el spec connected-storage-matrix, prueba "a row deleted in unified mode stays gone across account churn and a reload". Su ayudante switchToUnified afirma de inmediato que el botón Todas las bandejas de la barra lateral existe y es visible, sin ninguna espera, y corre justo después de que la prueba anterior recargara la app. Se esperaba true, se recibió false. No pudimos volver a correr ese spec solo después, porque cada intento de arrancar una sesión de driver nueva en ese checkout falló antes de llegar al driver, con un UND_ERR_INVALID_ARG de undici en la solicitud de sesión. Así que queda sin clasificar. Se lee como una espera que falta después de una recarga más que como un fallo del producto, y se queda en el vídeo.
1.8 segundos por caso frente a 8.3
539 casos de MailVault en 963 segundos de reloj son unos 1.8 segundos por caso. Los 49 casos ejecutados de MeatPad en 405.032 segundos son unos 8.3 segundos cada uno. Misma máquina, misma tarde, un factor de aproximadamente cinco entre ambos.
Dos razones estructurales, no una optimización ingeniosa. WebDriver ejecuta JavaScript dentro de la aplicación en marcha y lee el DOM directamente, así que una comprobación se resuelve contra uno de los 494 ganchos data-testid de los specs de extremo a extremo en lugar de cruzar un límite de proceso hacia el árbol de accesibilidad de macOS. Y la aritmética de lanzamientos es distinta: 539 casos de MailVault cuestan 81 lanzamientos de app, mientras que 50 casos de MeatPad cuestan 50. Medimos esa diferencia como es debido hace un par de semanas en Nota de campo 006.
Nada de esto hace que una app esté mejor probada que la otra. Las suites comprueban productos distintos, y 8.3 segundos por caso es el precio de conducir una interfaz nativa por la única vía que macOS ofrece desde fuera del proceso.
Corre las pruebas tú mismo
git clone https://github.com/GraphicMeat/MeatPad
cd MeatPad
brew install xcodegen
xcodegen generate
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
-only-testing:MeatPadUITests
No te saltes la línea de xcodegen, por la razón de arriba. El código fuente de MeatPad está en GitHub. La suite de MailVault es un clon más y el mismo comando que corrió en cámara:
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
Todo es público, el fallo intermitente de MeatPad y el único fallo de MailVault incluidos, y el código fuente de MailVault está junto al de MeatPad en GitHub.
Software para Mac local-first, con las mediciones adjuntas. MailVault guarda tu correo en un vault en tu propio disco, y una máquina hace clic por toda la app antes de cada lanzamiento.
Ver MailVault →