Un archiveur de courrier n'a qu'une seule façon d'échouer qui compte vraiment : faire subir à votre courrier quelque chose que vous n'avez pas demandé. Marquer un message comme lu, le classer dans le mauvais dossier, supprimer la mauvaise chose. Alors, chaque fonctionnalité de MailVault est livrée de la même façon : seulement après qu'une machine a ouvert l'application réelle, exécuté la fonctionnalité comme le ferait une personne, et lu le résultat directement sur l'écran.

Ceci est une note d'avancement sur la façon dont cela fonctionne, parce que la mise en place s'est révélée plus intéressante que les fonctionnalités.

Les tests utilisent l'application. Ils ne la simulent pas.

MailVault est une application Tauri : un backend Rust qui fait le travail IMAP, une interface React dans une webview native. Tauri a ici un super-pouvoir sous-exploité : construire l'application avec sa fonctionnalité webdriver et l'application réelle devient contrôlable à distance, de la même façon que le sont les navigateurs. Notre suite tourne sur WebdriverIO avec sa capacité wry via tauri-driver, contre le binaire réellement compilé.

La distinction compte. Un test de composant fait le rendu d'une liste dans un navigateur imaginaire et vérifie une sortie virtuelle. Nos tests de bout en bout lancent la même fenêtre que celle qu'obtient un utilisateur, attendent que les mêmes lignes s'affichent, cliquent sur les mêmes boutons, puis vérifient ce que l'interface et le courrier sur le serveur disent réellement ensuite. Un extrait d'une exécution cette semaine, testant la barre d'action de sélection :

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)

Chaque ligne est un aller-retour complet : un clic dans l'interface, un appel au backend Rust, une conversation IMAP avec un serveur, et un réaffichage que le test vérifie de ses propres yeux. Quand la suite dit que l'archivage fait passer l'icône de source à local, cela veut dire qu'une machine a observé ce changement d'icône.

Les serveurs de courrier naissent et meurent avec l'exécution

Rien de tout cela ne touche un compte réel. Chaque exécution démarre de nouveaux serveurs IMAP fictifs sur des ports locaux et les alimente avec deux comptes connus, afin que la suite puisse exercer le changement de compte et la boîte de réception unifiée plutôt qu'un seul chemin idéal. Un compte contient délibérément sept cents messages, davantage que les deux fenêtres de chargement de l'application, de sorte qu'il y a toujours une boîte aux lettres véritablement chargée partiellement, l'état où vivent les bugs de pagination.

L'application elle-même pointe vers un répertoire personnel jetable créé pour l'exécution. Son archive, ses réglages, sa base de données locale, tout atterrit dans un dossier qui n'existait pas une minute plus tôt et qui sera supprimé à la fin de l'exécution. Aucun identifiant, aucun réseau, aucun courrier réel à proximité du processus.

Le déterminisme est l'autre raison. Les vrais serveurs de courrier vous rationnent. Gmail plafonne les téléchargements IMAP à 2 500 MB par jour, ce dont nous avons parlé dans la Note de terrain précédente, et chaque fournisseur est capricieux d'une façon qu'une suite de tests ne peut pas se permettre. Un serveur fictif fait exactement ce que dit le scénario, à chaque fois, en quelques millisecondes. Le travail de la suite est d'attraper nos bugs, pas la météo de Google.

Un client de messagerie testé comme s'il contenait quelque chose qui compte. MailVault archive votre courrier dans des fichiers standards sur votre propre disque, et chaque version doit d'abord mériter son passage par cette suite.

Visitez MailVault

La machine sous le bureau n'est pas la machine avec laquelle nous travaillons

Voici la partie qui a changé le quotidien. Une exécution complète de bout en bout est lourde : elle construit le frontend React, compile le backend Rust, puis ouvre et ferme l'application des dizaines de fois : une session neuve par fichier de spec, quatorze fichiers de spec rien que pour la suite connectée. Faites tourner cela sur la machine de développement et vous regardez les fenêtres voler le focus pendant que le CPU oublie votre existence. Personne n'écrit de code pendant ce temps-là.

Alors les exécutions n'ont pas lieu là. Un petit Mac mini Apple Silicon reste sur le réseau local à ne rien faire d'autre. Il n'est accessible que par SSH, uniquement depuis l'intérieur du réseau, et il détient un clone du dépôt et de la chaîne d'outils. Quand une fonctionnalité est prête à faire ses preuves, l'exécution est envoyée au mini avec une seule commande, et la machine de développement retourne construire la prochaine chose pendant que le mini compile, démarre et clique à travers celle en cours.

Les deux activités se chevauchent au lieu d'alterner, ce qui paraît anodin jusqu'à ce qu'on compte combien de fois par jour une suite tourne pendant que des fonctionnalités sont en cours de développement. Chaque session sur le mini démarre l'application à froid, avec un état neuf et des serveurs neufs, ce qui est aussi un test honnête du démarrage à froid, sur un matériel commodément plus lent que la machine sur laquelle l'application a été écrite. Si une fonctionnalité est lente sur le mini, ce n'est pas le problème du mini. C'est un aperçu du portable vieux de quatre ans de quelqu'un.

La sortie d'échec revient de la même façon qu'en local : quelle spec, quelle assertion, ce que l'interface a réellement affiché. On corrige, on relance. La boucle est ennuyeuse, et l'ennui est tout l'intérêt.

Ce que cela attrape et que les tests unitaires ratent poliment

Les meilleures trouvailles de la suite ne se situent jamais dans la couche qu'un test unitaire couvrirait. Elles vivent dans les coutures : un fil de discussion ouvert depuis la boîte de réception qui affiche le corps de la réponse depuis la mauvaise boîte aux lettres, un message déplacé qui a quitté le serveur mais pas la liste, une action d'archivage qui a fonctionné mais a réaffiché l'icône de la mauvaise ligne. Chacun de ces cas, ce sont trois sous-systèmes qui s'accordent à être individuellement corrects et collectivement faux. Le seul test qui les attrape est celui qui fait ce que fait l'utilisateur.

C'est pourquoi les specs sont écrites comme des phrases utilisateur (« marks a selected row as read and repaints it ») plutôt que comme des noms de fonctions. Quand l'une d'elles échoue, le rapport se lit comme un bug qu'une personne signalerait.

Exécutez la suite vous-même

MailVault est open source, et la suite tourne entièrement contre les serveurs fictifs : aucun compte, aucun identifiant, rien à configurer. Si vous avez Node 18 ou plus récent et une chaîne d'outils Rust installée :

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

L'installation cargo fournit tauri-wd, le pont WebDriver que la suite lance pour piloter l'application. La dernière commande fait le reste : elle construit le frontend en mode test, compile le backend Rust avec sa fonctionnalité webdriver puis démarre les serveurs IMAP fictifs et fait avancer l'application à travers chaque spec. Le premier lancement passe le plus clair de son temps dans le compilateur Rust ; ensuite, npm run test:e2e:ui et npm run test:e2e:connected réutilisent la compilation et exécutent leurs suites directement.

Pendant qu'elle tourne, la fenêtre de l'application va s'ouvrir, cliquer toute seule, et se fermer, encore et encore. Ce qui est soit troublant, soit tout l'intérêt, selon ce que vous pensez d'un robot qui lit du courrier.

Ce que nous n’avons pas encore résolu

L'honnêteté coûte moins cher que le marketing, alors : les serveurs fictifs parlent l'IMAP que nous avons implémenté, pas tous les dialectes que Gmail et Outlook improvisent, et la connexion OAuth contre les fournisseurs réels est encore vérifiée par une main humaine. Les suites de régression visuelle et de sauvegarde tournent encore manuellement plutôt qu'à chaque envoi. Et le mini exécute une seule suite à la fois, séquentiellement : seize gigaoctets de mémoire, c'est un budget, et nous préférons une exécution fiable à deux instables. Les trois sont sur la liste, dans cet ordre.

Pendant ce temps, les fonctionnalités continuent d'arriver de la façon lente : écrites, envoyées, parcourues en cliquant par un petit ordinateur qui ne s'ennuie jamais, et livrées seulement quand il arrête de trouver des choses. Votre courrier mérite au moins ce niveau de cérémonie.

Votre courrier, archivé localement, par une application testée plus durement qu'elle n'est commercialisée. Gratuit et open source pour macOS et Linux.

Visitez MailVault