Debaixo da mesa há um Mac mini com Apple M4 e 16 GB de memória, e o trabalho dele é descobrir se nossos aplicativos ainda funcionam. MailVault é feito com Tauri. MeatPad é feito com SwiftUI. O mini roda as duas suítes: mesma máquina, mesma tarde, mesma pessoa escrevendo os testes.
As duas suítes não custam a mesma coisa. Não custam nada parecido. Tudo o que vem abaixo foi medido em 25 de agosto de 2026, e cada comando é um que você mesmo pode rodar.
O que ficou verde hoje
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
Leia as duas últimas linhas de cada bloco uma contra a outra, porque é aí que está o artigo inteiro. O MailVault consegue 1.440 verificações sem janela em menos de sete segundos de relógio, e ainda pode conduzir o próprio binário real por 53 specs de ponta a ponta. O MeatPad consegue 601 verificações sem janela em onze segundos, e 31 testes que tocam a interface de verdade.
E há o número que não cabe numa tabela. Rodar oito daqueles 31 testes de interface no mini levou 98,186 segundos. Os casos individuais ficaram entre 6,9 e 17,2 segundos. Chame de doze segundos por teste.
Executed 8 tests, with 0 failures (0 unexpected) in 98.186 seconds
Um teste unitário do MeatPadKit na mesma máquina fica em média em dezenove milissegundos. Um caso de vitest do MailVault fica em média abaixo de dois. Um único clique simulado numa view SwiftUI custa cerca de seiscentos testes unitários.
Agosto, em duas curvas
O MailVault passou o mês recebendo testes, e o formato disso fica fácil de ver contando os casos de teste do repositório 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
A contagem de casos quase dobrou em vinte e cinco dias, e as specs de ponta a ponta mais que dobraram. Esse crescimento não está distribuído de forma uniforme pelo aplicativo. Ele foi para onde estavam os bugs: redação e rascunhos, seleção em massa, a matriz de armazenamento que precisa concordar sobre o que está em disco depois de cada ação que altera algo, e a troca de conta. Os títulos de commit daquele trecho se leem como uma lista de confissões, que é como um registro de testes deve mesmo se ler.
A curva do MeatPad é mais curta, porque o aplicativo é mais novo, e tem nela um degrau sobre o qual vale a pena 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
O MeatPad saiu em julho com 517 testes e nenhum deles abria uma janela. O primeiro teste de interface foi escrito em 24 de agosto, um mês depois do lançamento. Essa lacuna não é preguiça. É o assunto do resto deste artigo.
Um arquivo de e-mails que você consegue mesmo conferir. O MailVault faz backup e arquiva sua caixa de correio localmente, e 53 specs de ponta a ponta conduzem o aplicativo real antes que qualquer coisa disso chegue até você.
Visite MailVault →A diferença que explica todas as outras
A interface de um app Tauri é uma página web. O arcabouço de testes fala WebDriver com ela, o que significa que o arcabouço pode executar código dentro do próprio aplicativo em execução:
const searchExists = await browser.execute(() => {
const input = document.querySelector('input[placeholder*="Search"]');
return input !== null;
});
Aquela função seta não é avaliada pelo teste. Ela é enviada para a webview do próprio aplicativo, executada ali contra o documento vivo, e o resultado volta. O teste consegue interrogar a interface exatamente como o próprio aplicativo consegue.
O XCUITest não consegue fazer isso e nunca vai conseguir. Ele é um processo separado, conduzindo seu aplicativo de fora pela camada de acessibilidade do macOS. Não existe canal para código arbitrário, nem jeito de fazer a uma view uma pergunta que a árvore de acessibilidade já não responda. Tudo o que segue é consequência dessa única fronteira.
Num documento, tudo tem alça
A suíte de ponta a ponta do MailVault seleciona elementos 305 vezes por meio de atributos de identificador de teste. Acrescentar um não custa nada e não muda nada no jeito como o aplicativo roda. Tudo o que o aplicativo desenha é endereçável, tenha alguém planejado isso ou não.
O MeatPad tem 34 identificadores de acessibilidade em toda a sua árvore de código, e cada um deles teve de ser deliberadamente pendurado numa view dentro do código de produção que é publicado:
TextField("Filter", text: $query)
.accessibilityIdentifier("board.labelFilter")
Tudo o que não tiver um, para efeito de testes, não está lá. A superfície de teste de um app SwiftUI não é «a interface». É «as partes da interface que alguém lembrou de nomear», e essa lista só cresce quando um teste está sendo escrito.
E você não pode perguntar que tipo de coisa foi que você nomeou
Mesmo com um identificador, uma view SwiftUI não promete a você no que ela se transformou. É assim que os testes do MeatPad alcançam o campo de busca do quadro:
app.descendants(matching: .any).matching(identifier: "board.search").firstMatch
Casar com qualquer tipo de descendente é uma rendição. No código-fonte está escrito TextField, mas se ele aparece como campo de texto, campo de busca ou algo completamente diferente depende de como o SwiftUI escolheu construí-lo naquele dia, e isso pode mudar com uma atualização do sistema. Varrer todo tipo de elemento é mais lento e mais vago do que pedir aquilo que você escreveu, e é a única opção confiável.
Ler o valor de volta é a mesma história. O título de um cartão volta como uma conversão opcional sobre um valor de tipo desconhecido, porque é só isso que a camada de acessibilidade oferece, e acertar no palpite é trabalho seu.
Ninguém espera por você
O WebDriver já traz a espera embutida. O XCUITest espera um elemento existir e não oferece absolutamente nada para uma condição que envolva vários. Então os testes do MeatPad fazem isso na mão:
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
}
Os cartões deixam a hierarquia de views um ou dois quadros depois da tecla que os filtrou, então uma única asserção logo após digitar é cara ou coroa. Todo teste que confere uma lista depois de uma interação precisa desse laço, e cada um desses laços é um lugar onde uma falha de verdade pode passar dez segundos parecendo um sucesso lento.
Colocar o aplicativo num estado conhecido
A suíte do MailVault sobe um servidor IMAP simulado por conta com uma caixa de entrada preparada com 700 mensagens, aponta o aplicativo para elas, e dá à execução inteira um diretório pessoal descartável. O aplicativo sob teste está completamente inalterado. Ele não faz ideia de que está sendo testado.
Ao MeatPad é preciso avisar. Seus testes de interface escrevem um diretório de armazenamento no disco e então abrem o aplicativo com argumentos que o binário publicado entende:
app.launchArguments = [
"-meatpad.storageRootOverride", storageRoot.path,
"-meatpad.revealBoard", boardID.uuidString,
"-hasSeenFirstRunIntro", "YES",
]
Essas flags são código de verdade no aplicativo publicado, existindo para que testes consigam alcançar uma tela. É uma troca justa e faríamos de novo, mas vale dizer com todas as letras: com o XCUITest, parte do seu arcabouço de testes é entregue aos usuários.
Onde os testes têm permissão de rodar
A suíte de ponta a ponta sem janela do MailVault roda num runner comum do GitHub Actions com Ubuntu. O binário Tauri de verdade é compilado para Linux, aberto dentro de um framebuffer virtual e de uma sessão de barramento de mensagens, e clicado pelo WebDriver em hardware que não custa nada por minuto. O encantamento exato está logo abaixo.
dbus-run-session -- xvfb-run --auto-servernum \
--server-args="-screen 0 1920x1080x24" \
npx wdio run wdio.conf.js --suite ui-headless
Não existe linha equivalente para o XCUITest. Ele precisa de macOS, de um servidor de janelas e de uma sessão de usuário real, porque está de fato movendo um ponteiro e pressionando teclas. O repositório do MeatPad tem exatamente um workflow do GitHub e ele compila versões. Os testes moram no mini, que é a resposta honesta para «onde um estúdio pequeno roda testes de interface para Mac», e a razão de aquela máquina existir. Já contamos como ela se paga para o MailVault em uma nota anterior.
A parte do MeatPad que nenhum teste de interface jamais vai alcançar
Olhe de novo onde estão os 31 testes de interface do MeatPad: exibição de cartões, editor de cartão, etiquetas, quebras de linha, busca. Tudo isso é o quadro. Nenhum deles toca o editor, que é o produto de verdade.
O editor embrulha o STTextView sobre o TextKit 2 dentro de um NSViewRepresentable, e para a árvore de acessibilidade ele é uma única região opaca contendo texto. Múltiplos cursores, marcadores de trecho, dobra de código, realce incremental com tree-sitter, correspondência de parênteses: nada disso existe como elementos consultáveis. Você não consegue afirmar que o segundo cursor caiu na linha quarenta, porque não existe nenhum elemento chamado «o segundo cursor».
Então esse trabalho não é testado através da interface. É testado por baixo dela. O MeatPadKit é um pacote Swift separado, 47 arquivos de código, que guarda o varredor de dobras, o modelo multicursor, o analisador de trechos, o realçador, o comparador aproximado, a ponte de posições do LSP e o índice de símbolos do projeto. Não tem código de view nenhum, e é exatamente por isso que 601 testes conseguem rodar contra ele em onze segundos sem nenhuma janela na tela.
O comentário no topo de um dos testes de interface do MeatPad coloca essa divisão melhor do que saberíamos parafrasear: o MeatPadKit testa unitariamente o próprio comparador, e o que ele não alcança é se o campo está sequer ligado às colunas. Essa é a descrição completa do cargo de um teste de interface SwiftUI. Não «a lógica funciona», que um teste rápido sem janela já respondeu, mas «está ligado», que nada mais consegue responder.
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 que isso muda de verdade no jeito como você constrói
A lição não é que o SwiftUI seja ruim. É que o custo de verificar uma interface nativa é alto o bastante para ser uma entrada de arquitetura em vez de uma lembrança tardia.
- Empurre a lógica para fora da view até a view ficar sem graça. O MeatPad tem 51 arquivos de código de interface e 47 de framework, e o lado do framework carrega 95 por cento dos testes. Tudo o que ficar numa view SwiftUI é caro de conferir, então a quantidade certa a deixar ali é o layout.
- Gaste os testes de interface na ligação, não no comportamento. A doze segundos cada, eles são a ferramenta errada para casos-limite e a ferramenta certa para «o campo de busca está ligado à coluna».
- Acrescente os identificadores quando escrever a view, não quando escrever o teste. Colocá-los depois significa editar arquivos de produção para tornar um teste possível, que é exatamente quando as pessoas param de escrever testes.
- Se a sua interface é uma webview, aceite o almoço grátis. Poder rodar uma consulta dentro do seu próprio aplicativo em execução é uma vantagem genuína da pilha Tauri, e é por isso que o MailVault conseguiu acrescentar 31 specs de ponta a ponta num único mês.
O que não está coberto
Duas lacunas assumidas, já que um artigo sobre testes que só relata as vitórias é um folheto.
O editor do MeatPad não tem nenhuma cobertura no nível da interface. A lógica por baixo é testada a fundo, e a cola entre essa lógica e a view de texto é conferida por uma pessoa. Uma regressão ali chegaria a uma versão publicada. Ir roendo isso com um número pequeno de testes caros, nas interações que valem doze segundos cada, é o próximo passo.
Do lado do MailVault, só a suíte sem janela roda na integração contínua. As 41 specs preparadas e as 5 locais rodam no mini, sob demanda, o que quer dizer que elas pegam as coisas antes de uma publicação e não antes de um merge. E uma suíte verde continua provando apenas as coisas que alguém pensou em conferir, e é por isso que o número naquela tabela importa menos do que o fato de ele continuar se mexendo.
Reproduza qualquer um desses números
# 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
O último comando é o que leva dois minutos. Agora você sabe por quê.
Dois aplicativos, uma máquina que os mantém honestos. Software para Mac local-first, de um estúdio que publica suas medições, inclusive as que não o favorecem.
Veja o que construímos →