Um arquivador de e-mail tem exatamente uma forma de falhar que importa: fazer com seu e-mail algo que você não pediu. Marcar uma mensagem como lida, arquivá-la na pasta errada, excluir a coisa errada. Por isso, todo recurso do MailVault é lançado da mesma forma: só depois que uma máquina abre a aplicação de verdade, executa o recurso do jeito que uma pessoa faria, e lê o resultado direto na tela.

Esta é uma nota de progresso sobre como isso funciona, porque a estrutura acabou sendo mais interessante do que os recursos em si.

Os testes usam o app. Eles não o simulam.

MailVault é uma aplicação Tauri: um backend em Rust cuidando do trabalho de IMAP, uma interface em React dentro de um webview nativo. O Tauri tem aqui um superpoder pouco aproveitado: compilar o app com seu recurso webdriver e a aplicação de verdade se torna controlável remotamente, do mesmo jeito que os navegadores são. Nossa suíte roda no WebdriverIO com o recurso wry e via tauri-driver, contra o binário de fato compilado.

A distinção importa. Um teste de componente renderiza uma lista em um navegador imaginário e faz asserções sobre uma saída virtual. Nossos testes ponta a ponta abrem a mesma janela que o usuário recebe, esperam as mesmas linhas serem pintadas, clicam nos mesmos botões e então verificam o que a interface e o e-mail no servidor realmente dizem depois. Um trecho de uma execução desta semana, testando a barra de ações de seleção:

Selection Action Bar effects
   ✓ marks a selected row as read and repaints it
   ✓ marks a row back as unread
   ✓ marks several selected rows as read in one action
   ✓ archives a selected row and flips its source icon to local
   ✓ unarchives it again
   ✓ moves a selected row to another folder and drops it from the list
   ✓ deletes a selected row from the server and drops it from the list

7 passing (833ms)

Cada linha é uma ida e volta completa: um clique na interface, uma chamada ao backend em Rust, uma conversa IMAP com um servidor e uma repintura que o teste verifica com os próprios olhos. Quando a suíte diz que arquivar troca o ícone de origem para local, significa que uma máquina observou aquele ícone mudar.

Os servidores de e-mail nascem e morrem com a execução

Nada disso toca em uma conta real. Cada execução inicia servidores IMAP simulados novos em portas locais e os popula com duas contas conhecidas, para que a suíte possa exercitar a troca de contas e a caixa de entrada unificada em vez de um único caminho feliz. Uma das contas guarda deliberadamente setecentas mensagens, maior que as duas janelas de carregamento do app, de modo que sempre existe uma caixa de entrada genuinamente carregada pela metade, o estado onde vivem os bugs de paginação.

O próprio app é apontado para um diretório home descartável criado para a execução. Seu arquivo, suas configurações, seu banco de dados local: tudo vai parar em uma pasta que não existia um minuto antes e será apagada quando a execução terminar. Nenhuma credencial, nenhuma rede, nenhum e-mail real perto do processo.

O determinismo é o outro motivo. Servidores de e-mail reais racionam você. O Gmail limita os downloads via IMAP a 2.500 MB por dia, sobre o que escrevemos na Field Note anterior, e todo provedor é instável de formas que uma suíte de testes não pode se dar ao luxo de tolerar. Um servidor simulado faz exatamente o que o cenário manda, todas as vezes, em milissegundos. O trabalho da suíte é pegar nossos bugs, não o clima do Google.

Um cliente de e-mail testado como se guardasse algo que importa. O MailVault arquiva seu e-mail em arquivos padrão no seu próprio disco, e todo lançamento precisa primeiro merecer sua passagem por essa suíte.

Visite MailVault

A máquina embaixo da mesa não é a máquina em que trabalhamos

Aqui está a parte que mudou o dia a dia. Uma execução ponta a ponta completa é pesada: ela compila o frontend em React, compila o backend em Rust, e depois abre e fecha a aplicação dezenas de vezes: uma sessão nova por arquivo de especificação, quatorze arquivos de especificação só na suíte conectada. Rode isso na máquina de desenvolvimento e você vai ver janelas roubando o foco enquanto a CPU esquece que você existe. Ninguém escreve código durante isso.

Por isso as execuções não acontecem ali. Um pequeno Mac mini com Apple Silicon fica na rede local sem fazer mais nada. Ele só é acessível via SSH, só de dentro da rede, e guarda um clone do repositório e do toolchain. Quando um recurso está pronto para se provar, a execução é despachada para o mini com um único comando, e a máquina de desenvolvimento volta a construir a próxima coisa enquanto o mini compila, inicializa e clica através da atual.

As duas atividades se sobrepõem em vez de se alternar, o que soa como uma coisa pequena até você contar quantas vezes por dia uma suíte roda enquanto recursos estão sendo desenvolvidos. Cada sessão no mini inicia o app do zero, com estado novo e servidores novos, o que também é um teste honesto de inicialização a frio, em um hardware convenientemente mais lento do que a máquina em que o app foi escrito. Se um recurso fica lento no mini, o problema não é do mini. É uma prévia do laptop de quatro anos de alguém.

A saída de falha volta do mesmo jeito que voltaria localmente: qual especificação, qual asserção, o que a interface realmente mostrou. Corrige, despacha de novo. O ciclo é entediante, e entediante é exatamente o objetivo.

O que isso pega que testes unitários educadamente deixam passar

As melhores descobertas da suíte nunca estão na camada que um teste unitário cobriria. Elas vivem nas costuras: uma conversa aberta a partir da caixa de entrada mostrando o corpo da resposta da caixa errada, uma mensagem movida que saiu do servidor mas não da lista, uma ação de arquivamento que funcionou mas repintou o ícone da linha errada. Cada uma dessas é o resultado de três subsistemas concordando em estar individualmente corretos e coletivamente errados. O único teste que os pega é aquele que faz o que o usuário faz.

É por isso que as especificações são escritas como frases de usuário ("marca uma linha selecionada como lida e a repinta") em vez de nomes de função. Quando uma falha, o relatório se lê como um bug que uma pessoa registraria.

Rode a suíte você mesmo

O MailVault é de código aberto, e a suíte roda inteiramente contra os servidores simulados: sem contas, sem credenciais, nada para configurar. Se você tem Node 18 ou mais recente e um toolchain de Rust instalados:

git clone https://github.com/GraphicMeat/mail-vault-app
cd mail-vault-app
npm install
cargo install tauri-webdriver-automation
npm run test:e2e

O cargo install fornece tauri-wd, a ponte WebDriver que a suíte inicia para controlar o app. O último comando faz o resto: compila o frontend em modo de teste, compila o backend em Rust com o recurso webdriver e inicia os servidores IMAP simulados e conduz o app por todas as especificações. A primeira execução passa a maior parte do tempo no compilador do Rust; depois disso, npm run test:e2e:ui e npm run test:e2e:connected reaproveitam a build e rodam suas suítes diretamente.

Enquanto roda, a janela do app vai abrir, clicar sozinha e fechar, repetidamente. O que é ou perturbador ou exatamente o ponto, dependendo de como você se sente em relação a um robô lendo e-mail.

O que ainda não resolvemos

Honestidade é mais barata que marketing, então: os servidores simulados falam o IMAP que implementamos, não todo dialeto que Gmail e Outlook improvisam, e o login OAuth contra os provedores reais ainda é verificado por uma mão humana. As suítes de regressão visual e de backup ainda rodam manualmente em vez de em cada despacho. E o mini roda uma suíte de cada vez, sequencialmente: dezesseis gigabytes de memória são um orçamento, e preferimos ter uma execução confiável a duas instáveis. As três estão na lista, nessa ordem.

Enquanto isso, os recursos continuam chegando do jeito lento: escritos, despachados, clicados por um computador pequeno que não fica entediado, e lançados só quando ele para de encontrar problemas. Seu e-mail merece pelo menos essa cerimônia.

Seu e-mail, arquivado localmente, por um app que é testado com mais rigor do que é divulgado. Gratuito e de código aberto para macOS e Linux.

Visite MailVault