一个邮件归档工具只有一种真正要命的失败方式:对你的邮件做了你没有要求的事:把一封邮件标记为已读、归档到错误的文件夹、删错东西。所以, MailVault 中的每一个功能都以同样的方式发布:只有当一台机器打开了真实应用、像人一样执行了该功能、并从实际屏幕上读取结果之后。
这是一篇关于其运作方式的进展记录,因为这套搭建本身,比功能本身更有意思。
测试使用的是应用本身,而不是对它的模拟。
MailVault 是一个 Tauri 应用:Rust 后端负责 IMAP 工作,React 界面运行在原生 webview 中。Tauri 在这里有一个被低估的超能力:用它的 webdriver 特性构建应用,真实的应用就会变得可以远程控制,就像浏览器一样。我们的测试套件运行在 WebdriverIO 上,使用 wry 能力,通过 tauri-driver,针对实际编译出的二进制文件运行。
这个区别很重要。组件测试是在一个虚构的浏览器里渲染一个列表,并对虚拟输出做断言。我们的端到端测试则打开用户实际会看到的同一个窗口,等待同样的行渲染出来,点击同样的按钮,然后检查界面和服务器上的邮件实际显示了什么。以下是本周一次运行的节选,测试的是选择操作栏:
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)
每一行都是一次完整的往返:界面上的一次点击、一次 Rust 后端调用、一次与服务器的 IMAP 会话,以及测试用自己的眼睛核实的一次重绘。当测试套件说归档操作会把来源图标切换为本地时,意味着有一台机器亲眼看着那个图标发生了变化。
邮件服务器随每次运行而生,也随之而灭
这一切都不会碰到真实账户。每次运行都会在本地端口上启动全新的模拟 IMAP 服务器,并预置两个已知账户,这样测试套件就能验证账户切换和统一收件箱,而不只是单一的理想路径。其中一个账户特意装了七百封邮件,超过应用两个加载窗口的大小,这样就总有一个邮箱处于真正的部分加载状态,而分页 bug 就藏在这种状态里。
应用本身指向的是为这次运行而创建的一次性主目录。它的存档、设置、本地数据库,全都落在一分钟前还不存在、运行结束后就会被删除的文件夹里。整个过程附近没有任何凭据、没有网络、也没有任何真实邮件。
确定性是另一个原因。真实邮件服务器会对你限量配给。Gmail 把 IMAP 下载限制在每天 2,500 MB,这一点我们在 上一篇现场笔记中写过,而且每一家服务商都有各自善变的脾气,是测试套件无法承受的。模拟服务器每次都精确按照场景设定行事,耗时以毫秒计。测试套件的职责是抓住我们自己的 bug,不是 Google 的心情。
一个被当作真正重要之物来测试的邮件客户端。 MailVault 把你的邮件归档为你自己磁盘上的标准文件,每个版本发布之前,都要先在这套测试里挣得资格。
访问MailVault →桌下那台机器,不是我们平时工作用的机器
这是真正改变日常工作方式的部分。一次完整的端到端运行很重:它要构建 React 前端、编译 Rust 后端,然后把应用打开又关闭几十次:每个测试文件对应一次全新会话,光是已连接测试套件就有十四个测试文件。要是在开发机器上跑这些,你就会看着窗口不断抢夺焦点,CPU 忙到把你忘了。没人能在这期间写代码。
所以这些运行不会发生在那台机器上。一台小型 Apple Silicon Mac mini 安安静静地待在本地网络里,什么别的事都不做。它只能通过 SSH 访问,而且只能从网络内部访问,里面存着代码仓库和工具链的一份克隆。功能准备好接受检验时,运行会用一条命令派发到这台 mini 上,开发机器则回去构建下一件事,而 mini 负责构建、启动,并点击测试当前这一个。
这两件事因此可以并行,而不是轮流进行。听起来是小事,但算一算功能开发期间测试套件一天要跑多少次,就知道分量了。mini 上的每次会话都是冷启动应用,状态和服务器全都是全新的,这同时也是对冷启动的一次真实测试,而且是在一台恰好比开发这个应用所用的机器更慢的硬件上。如果某个功能在这台 mini 上跑得慢,那就不是 mini 的问题,而是某个人那台用了四年的笔记本电脑的预演。
失败信息回传的方式和在本地跑一模一样:哪个测试文件、哪个断言、界面实际显示了什么。修好,再派发一次。这个循环很枯燥,而枯燥正是它的全部意义所在。
它能抓住哪些单元测试悄悄漏掉的问题
这套测试套件最出色的发现,从来都不在单元测试能覆盖的那一层。它们藏在接缝处:从收件箱打开一个会话,却显示了另一个邮箱里回复的正文;一封被移动的邮件从服务器上消失了,却还留在列表里;一次归档操作本身成功了,却重绘错了行的图标。这些问题的共同点是:三个子系统各自都表现正确,合在一起却是错的。唯一能抓住它们的测试,就是那种做用户所做之事的测试。
这就是为什么测试用例是用用户式的句子写的(比如“把选中的一行标记为已读,并重新绘制它”),而不是函数名。当某一条失败时,报告读起来就像一个真实用户会提交的 bug。
自己跑一遍这套测试
MailVault 是开源的,整套测试完全针对模拟服务器运行:不需要账户,不需要凭据,什么都不用配置。如果你已经装好了 Node 18 或更新版本,以及 Rust 工具链:
git clone https://github.com/GraphicMeat/mail-vault-app
cd mail-vault-app
npm install
cargo install tauri-webdriver-automation
npm run test:e2e
cargo install 会提供 tauri-wd,也就是测试套件用来控制应用的 WebDriver 桥接程序。最后一条命令完成剩下的工作:以测试模式构建前端,用 webdriver 特性编译 Rust 后端,启动模拟 IMAP 服务器,并驱动应用执行每一个测试。第一次运行大部分时间花在 Rust 编译器上;此后, npm run test:e2e:ui 和 npm run test:e2e:connected 会复用已构建的产物,直接运行各自的测试套件。
运行期间,应用窗口会一遍又一遍地打开、自己点击自己、再关闭。这要么让人不安,要么正是重点所在,取决于你怎么看待一个读邮件的机器人。
我们还没解决的问题
诚实比营销省事,所以:模拟服务器讲的是我们自己实现的那套 IMAP,而不是 Gmail 和 Outlook 各自即兴发挥的每一种方言,针对真实服务商的 OAuth 登录仍然由人工手动验证。视觉回归测试和备份测试套件依然是手动运行,而不是每次派发都跑。而且这台 mini 一次只跑一套测试,按顺序来:十六 GB 内存是一笔预算,我们宁愿要一次可信的运行,也不要两次不稳定的运行。这三件事都在计划之中,顺序就是上面这样。
与此同时,功能还是照老办法慢慢到来:写好、派发、由一台不会厌烦的小型计算机点着点着测下来,直到它不再发现问题才发布。你的邮件,值得这么点仪式感。
你的邮件,本地归档,由一个测试强度超过营销投入的应用来完成。 适用于 macOS 和 Linux,免费且开源。
访问MailVault →