Há um Mac mini com Apple M4 e 16 GB de memória numa prateleira, rodando macOS 15, Xcode 26.6 e Swift 6.3.3, acessível só por SSH na rede local. Em 5 de setembro de 2026, ele passou uma tarde rodando os testes de interface dos nossos dois aplicativos Mac, um atrás do outro, com a gravação de tela ligada. MeatPad, nosso bloco de notas e editor de código para macOS, é um aplicativo SwiftUI testado com XCUITest: 50 casos, começando às 12:58. MailVault, nosso arquivador de e-mail local, é um aplicativo Tauri testado com WebdriverIO: 539 casos, começando às 14:43. São 589 casos de teste gravados, numa única máquina, numa única tarde, e as duas execuções não se parecem em nada.
Ato um: MeatPad, cinquenta inicializações em 405 segundos
Um comando, à esquerda da tela, contra o MeatPad 0.15.0:
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
-only-testing:MeatPadUITests
Tudo o mais nessa gravação é consequência dessa linha. O Xcode compila o aplicativo e um segundo pacote chamado test runner, e entrega os dois à máquina. Para cada caso de teste, o runner inicia o aplicativo do zero, espera pela janela dele, move o ponteiro, pressiona teclas, lê o que voltou, e encerra o aplicativo. Depois faz tudo de novo para o próximo caso. As janelas aparecendo e sumindo não são um efeito visual. Esse é o loop.
Por que um caso de XCUITest custa segundos
A execução terminou 50 casos em 405.032 segundos. Um dos 50 se ignora de propósito, então os 49 que de fato rodaram levam em média cerca de 8.3 segundos cada. Compare isso com o MeatPadKit, o framework por baixo da interface, onde um teste leva em média dezenove milissegundos.
Três coisas pagam essa diferença. O aplicativo é iniciado uma vez por caso, então cada caso paga por uma partida a frio e pela janela se estabilizar. O teste roda num processo separado e só consegue ver o aplicativo pela árvore de acessibilidade do macOS, então toda pergunta sobre a tela é uma consulta através de uma fronteira de processos em vez de uma leitura de memória. E o ponteiro e o teclado são reais, então um arrasto demora o tempo que um arrasto demora.
Cinquenta mundos descartáveis
Um caso que clica num cartão precisa de um cartão para clicar. Nada é injetado no processo em execução, então o estado precisa existir antes do aplicativo iniciar. Cada caso escreve um diretório de armazenamento novo na pasta temporária do sistema, prepara um quadro dentro dele, e então inicia o aplicativo apontado para ele:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Essas três flags são código de verdade no binário de lançamento. Elas existem para que um teste consiga alcançar uma tela, e vão para todo mundo. Esse é o custo honesto de testar uma interface nativa de fora dela, e pagaríamos de novo, mas é melhor dizer isso em voz alta do que deixar alguém descobrir lendo o código-fonte. O lado bom é que a execução não deixa nada para trás: cada caso apaga seu diretório no teardown, então seus próprios quadros nunca são tocados, e uma falha não consegue contaminar o próximo caso.
Comece com uma nota. Fique pelo editor. O MeatPad é um caderno de notas e editor de código nativo do macOS: arquivos simples no seu Mac, sem conta, sem motor de sincronização, sem telemetria.
Conheça o MeatPad →O relógio, suíte por suíte
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
Doze suítes, cada uma batizada pelo que ela guarda: os três modos de exibição do cartão, o editor de cartão com suas amostras de cor e datas de vencimento, a edição de título no local e o arrastar de um cartão entre colunas, links dentro das notas do cartão, alvos de soltar, etiquetas e filtro de etiquetas, quebras de linha no campo de adição rápida, busca, o foco de teclado da folha de novo quadro, a dica de link do editor de notas, e os seis casos que perguntam ao Launch Services se o MeatPad é oferecido para cada tipo de arquivo que ele reivindica.
BoardLabel é a mais cara, com 102.638 segundos para 8 casos, porque o filtro de etiquetas remove cartões da vista um ou dois frames depois da tecla pressionada, então esses casos ficam num loop de polling esperando a coluna se estabilizar. O caso individual mais lento da execução foi 27.992 segundos, verificando que um título de cartão longo é cortado na exibição compacta e quebra nas outras duas, o que significa medir o mesmo título três vezes em três inicializações. O caso mais rápido foi 1.507 segundos, perguntando ao Launch Services se o MeatPad é oferecido para cada tipo de arquivo que ele registra, o que precisa do aplicativo rodando mas nunca toca na janela dele.
A linha de resumo que vai mentir para você
O projeto Xcode do MeatPad é gerado a partir de um manifesto e não é commitado. Isso é conveniente até o dia em que você adiciona um arquivo de teste, esquece de regenerar, e roda a suíte mesmo assim. Nada está errado do ponto de vista do xcodebuild. Ele compila o projeto que recebeu, roda os zero testes dentro dele, e imprime as duas palavras que todo mundo procura:
** TEST SUCCEEDED **
Uma execução que não rodou nada parece exatamente igual a uma que rodou tudo. Então a linha que vale a pena ler nunca é a última. É a contagem acima dela, que diz quantos casos de fato aconteceram. Aqui estão as últimas linhas da execução na gravação acima. O terminal no vídeo passa o xcodebuild por um grep e um sed, então ele imprime uma forma resumida da mesma saída e as linhas cabem ao lado da janela do aplicativo. O comando em si não muda:
[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 **
Cinquenta casos, um ignorado de propósito, nenhuma falha, 405.032 segundos. A contagem e a última linha concordam, que é a única combinação que vale a pena confiar.
O que aconteceu antes da tomada verde
A execução gravada não foi a primeira do dia. Uma não gravada, às 12:19, deu vermelho: os mesmos 50 casos, 1 ignorado, e 2 falhas, ambas no OpenWith, o caso de seleção múltipla e o caso do segundo arquivo, os dois que você vê passando em 3.799 e 4.066 segundos no trecho acima. Os relatórios de falha dizem que o aplicativo abortou 0.75 segundos depois de iniciar, dentro do próprio caminho de inicialização do SwiftUI, numa falha de precondição do AttributeGraph levantada a partir de applicationWillFinishLaunching enquanto o Dock ainda estava notificando o aplicativo. Nada tinha sido entregue a ele ainda, então isso não é o código de abrir arquivo. Esses mesmos 6 casos do OpenWith, rodados sozinhos logo depois, passaram 3 vezes em 3, o que são 18 casos em 18, e todos os 6 passaram de novo na execução gravada. Parece uma falha de temporização de inicialização que só aparece quando a suíte reinicia o aplicativo em sequência. Ainda está sob investigação, e não foi cortado de nada.
Uma coisa encontrada durante a preparação merece seu próprio parágrafo. Para caber o aplicativo ao lado do terminal, primeiro encolhemos as dimensões salvas da janela do MeatPad de 1440x900 pontos para 1010x900. Quatro casos falharam de uma vez: as amostras de cor no editor de cartão, um filtro de etiqueta e uma busca, todos puramente porque o elemento que eles buscavam estava fora da tela. Rodada de novo nas dimensões originais de 1440x900, o mesmo conjunto passou 9 de 9. O tamanho da janela é uma entrada para um teste de interface, e essa suíte assume silenciosamente as dimensões padrão do aplicativo. É por isso que a gravação mostra o MeatPad no tamanho padrão à direita e um terminal estreito à esquerda.
Duas tomadas anteriores foram para o lixo por motivos que não tinham nada a ver com o MeatPad: num deles, um diálogo de falha obsoleto, sobrado da execução das 12:19, ficou parado na tela; no outro, a suíte do MailVault de outro job pôs suas próprias janelas por cima na metade do caminho, porque o mini é uma máquina compartilhada. O que é uma boa introdução para a segunda metade da tarde.
Ato dois: MailVault, 81 arquivos de spec em dezesseis minutos
O MailVault 2.11.3 é feito da outra forma: um núcleo em Rust com uma interface web, empacotado pelo Tauri. Sua suíte de ponta a ponta é conduzida pelo WebdriverIO, e o comando às 14:43 pediu duas de suas três suítes:
npx wdio run wdio.conf.js --suite ui-headless --suite connected-ci
A suíte ui-headless tem 7 arquivos de spec cobrindo o estado de boas-vindas sem nenhuma conta configurada. A suíte connected-ci tem 74 arquivos de spec que rodam contra contas IMAP simuladas preparadas. São 81 arquivos de spec. Uma terceira suíte, local-manual, guarda mais 6 arquivos de spec para backup, migração, arquivamento e verificações visuais, e não fez parte desta execução.
Spec Files: 80 passed, 1 failed, 81 total (100% completed) in 00:16:03
Oitenta arquivos de spec passaram, um falhou, 81 no total, em 16 minutos e 3 segundos pelo próprio relógio do wdio. O vídeo dura 16:18 porque começa antes do comando e termina depois dele. Por baixo desses arquivos ficam 539 casos de teste: 533 passando, 5 ignorados e a falha única. A metade ui-headless contribuiu com 55 aprovados e 3 ignorados em cerca de 56 segundos de tempo de teste, e a connected-ci contribuiu com 478 aprovados, 1 falhando e 2 ignorados em cerca de 811 segundos.
O aplicativo é iniciado uma vez por arquivo de spec, não uma vez por caso, então os 539 casos do MailVault da tarde custam 81 inicializações. Um arquivo de spec leva em média cerca de 10.7 segundos e a mediana é 6 segundos, o que mostra que a média é puxada por um punhado de arquivos longos: connected-custody-claims com 54.3 segundos para 9 testes, connected-performance com 53.4 segundos para 7, connected-compose-editor com 39.6 segundos para 19, connected-compose-autosave com 38.7 segundos para 15. O arquivo mais rápido, connected-backup-partial-failure, rodou seus 5 testes em 32 milissegundos. O mais ocupado, connected-email-viewer, guarda 21 testes. Tudo isso se apoia numa base bem maior e bem mais barata: 2,683 testes unitários de vitest em 216 arquivos terminam em 6.77 segundos, 318 testes de Rust cobrem o núcleo, e as duas suítes gravadas são 518 casos it() no código-fonte.
Duas coisas na gravação merecem uma palavra. O terminal à esquerda imprime só as linhas RUNNING, PASSED e FAILED do wdio por arquivo, porque o wdio guarda seu relatório detalhado para bem o final da execução. E perto do fim, o spec de exportação abre uma mensagem exportada no Preview, o que cobre o terminal no último minuto; isso é o teste fazendo o trabalho dele, não um acidente.
O que o arcabouço prepara
Cada execução inicia seus próprios servidores IMAP simulados com três contas preparadas, luke, vader e yoda em mock.test, e o INBOX de vader guarda 700 mensagens, bem mais que as janelas de paginação do aplicativo, de modo que a paginação é de fato exercitada, não apenas presumida. Uma quarta conta é adicionada por um teste que dá para ver sendo digitado. O aplicativo sob teste é um build com o recurso webdriver habilitado, iniciado pelo tauri-wd, o crate tauri-webdriver-automation, e ele roda contra um diretório HOME descartável, então um vault de verdade nunca é tocado. Esse arcabouço tem sua própria descrição em Nota de campo 004.
A única falha, sem classificação
Um caso em 539 falhou: o spec connected-storage-matrix, teste "a row deleted in unified mode stays gone across account churn and a reload". Seu helper switchToUnified afirma imediatamente que o botão Todas as caixas da barra lateral existe e está visível, sem nenhuma espera, e ele roda logo depois que o teste anterior recarregou o aplicativo. Esperava-se true, recebeu-se false. Não conseguimos rodar de novo esse spec sozinho depois, porque toda tentativa de iniciar uma nova sessão de driver naquele checkout falhava antes de chegar ao driver, com um undici UND_ERR_INVALID_ARG na requisição de sessão. Então ele fica sem classificação. Parece mais uma espera faltando depois de um recarregamento do que um bug de produto, e ele permanece no vídeo.
1.8 segundos por caso contra 8.3
539 casos do MailVault em 963 segundos de relógio são cerca de 1.8 segundos por caso. Os 49 casos executados do MeatPad em 405.032 segundos são cerca de 8.3 segundos cada. Mesma máquina, mesma tarde, um fator de aproximadamente cinco entre eles.
Duas razões estruturais, não uma otimização esperta. O WebDriver executa JavaScript dentro do aplicativo em execução e lê o DOM diretamente, então uma verificação se resolve contra um dos 494 ganchos data-testid nos specs de ponta a ponta em vez de cruzar uma fronteira de processos até a árvore de acessibilidade do macOS. E a aritmética de inicializações é diferente: 539 casos do MailVault custam 81 inicializações do aplicativo, enquanto 50 casos do MeatPad custam 50. Medimos essa diferença direito há um par de semanas em Nota de campo 006.
Nada disso torna um aplicativo mais bem testado que o outro. As suítes verificam produtos diferentes, e 8.3 segundos por caso é o preço de conduzir uma interface nativa da única forma que o macOS oferece de fora do processo.
Rode você mesmo
git clone https://github.com/GraphicMeat/MeatPad
cd MeatPad
brew install xcodegen
xcodegen generate
xcodebuild test -scheme MeatPad -destination "platform=macOS" \
-only-testing:MeatPadUITests
Não pule a linha do xcodegen, pelo motivo explicado acima. O código-fonte do MeatPad está em GitHub. A suíte do MailVault é mais um clone e o mesmo comando que rodou na gravação:
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
Tudo isso é público, incluindo a falha do MeatPad e a única falha do MailVault, e o código-fonte do MailVault fica bem ao lado do MeatPad, em GitHub.
Software Mac local, com as medições anexadas. O MailVault guarda seu e-mail num cofre no seu próprio disco, e uma máquina clica em todo o aplicativo antes de cada lançamento.
Conheça o MailVault →