跳至内容
返回博客

我们如何测试 MailVault:600+ 项测试、真实邮件、真实服务器

6 分钟阅读

邮件属于那种表面上看起来简单、实际完全不是的东西。IMAP 服务器各有各的脾气。MIME 编码里的边界情况比多数开发者的职业生涯还老。OAuth2 令牌总在不合适的时候过期。附件可能嵌在 multipart 邮件里三层深。做邮件客户端,你要么把测试做到偏执,要么就发布 bug。

MailVault 目前有 600 多项自动化测试,分布在四层:Rust 单元测试、JavaScript 单元测试、针对真实 IMAP 服务器的集成测试,以及驱动真实桌面应用的端到端测试。下面是它们如何拼在一起的。

第 1 层:Rust 单元测试(55 项)

MailVault 的核心用 Rust 写成 — 所有 IMAP 连接、SMTP 发送、MIME 解析和 OAuth2 令牌管理都是原生运行,不经过 Node.js sidecar。Rust 这一层有 55 项单元测试,覆盖那些最难写对的函数。

一个很好的例子是 UID 区间压缩。当 MailVault 需要为 UID [1, 2, 3, 5, 6, 10] 获取邮件头时,它会把这些 UID 压缩成 IMAP 区间字符串 "1:3,5:6,10",好让命令尽可能短。测试覆盖了空输入、单个 UID、连续区间、有空隙的、未排序的、有重复的、超大区间,以及反向操作(把区间展开回单个 UID)。还有往返测试,验证 compress → expand 能还原出原来的集合。这些之所以要紧,是因为一旦区间算错,分块的 UID FETCH 命令就会出问题。

Microsoft Graph API 模块有自己的测试,验证把 Graph 的 JSON 响应解析成 MailVault 内部的 EmailHeader 格式;主模块的测试则覆盖 Maildir 文件名解析、标记编码,以及按 UID 查找文件。

第 2 层:JavaScript 单元测试(13 套)

前端有 13 套专门的单元测试跑在 Vitest 上,另有若干 store 层面的测试。它们覆盖的是夹在 Rust 后端和 React 界面之间的那部分逻辑:

邮件解析是测试最密集的一块。emailParser 这套测试会检验通信人提取(把“John Doe <[email protected]>”拆成姓名和地址)、主题归一化(剥掉 Re:、Fwd:、FW:、Re[N]: 的各种组合)、签名识别,以及引用层级判定。会话归并算法 — Jamie Zawinski 那套归并规范的简化版 — 有针对 In-Reply-To 链、References 头遍历、按主题兜底分组,以及畸形头部的环检测的测试。

其他几套分别测试后台流水线(并发控制、指数退避重试、当前账户与后台账户之间的级联顺序)、OAuth2 令牌处理(过期前主动刷新、并发刷新请求去重)、增量同步优化(四级 CONDSTORE 判定树)、快速加载流程(先立刻显示缓存的邮件头,再由后台同步补上变化)、抗故障加载(获取途中 IMAP 连接断开时的优雅降级),以及贯穿 Zustand store 的状态转换。

store 自身的测试则验证 LRU 缓存淘汰(MailVault 在内存里缓存邮件正文,上限可配)、字段剥离(缓存前去掉 rawSource 和附件内容以节省内存),以及缓存大小估算。

第 3 层:针对真实 IMAP 服务器的集成测试

带 mock 的单元测试能告诉你的东西有限。MailVault 的集成测试用真实凭据连接真实的 IMAP 服务器,并发送真实的邮件。我们在一个私有域名下维护两个专用测试账户,每次集成测试都会通过 TLS 与这两个账户通信。

核心的 email.test.js 会跑一整个闭环:通过 IMAP 连接,用 SMTP 发一封邮件(借助 Nodemailer),等它送达,用 mailparser 取回并解析,核对邮件头和正文,最后在清理阶段把这封测试邮件删掉。这能抓到 mock 抓不到的真实问题 — TLS 协商的怪癖、特定服务器的 IMAP 扩展、编码差异。

IMAP 优化套件拿真实服务器测试 MailVault 的高级协议特性:COMPRESS=DEFLATE 协商(服务器到底支不支持?压缩会话能不能正常工作?)、CONDSTORE 增量同步(CHANGEDSINCE 返回的变更标记对不对?),以及 ESEARCH 紧凑响应。

跨账户邮件套件测试多账户场景:从账户 A 发到账户 B,确认邮件出现在 B 的收件箱,检验已读/未读状态的传播,以及会话归并算法能否正确地把这条会话跨两个账户串起来。

500 封邮件的种子数据

为了贴近真实地测试,我们给测试邮箱预置 500 封邮件 — 两个账户之间每个方向各 250 封。种子脚本生成带真实感主题的邮件(「Q4 Revenue Report — Final Numbers」「Re: Bug in checkout flow」「Invoice #4821 — Due January 15」),并轮流使用五种 MIME 类型:纯文本、HTML、multipart/alternative、带单个附件的 HTML,以及带多个附件的 HTML(CSV、JSON 和 PNG)。

预置邮件中约有三分之一被标记为未读。附件包括生成的 PDF 报告、CSV 数据文件、JSON 元数据和 PNG 图片。这样我们就得到了一个看起来和用起来都像真实收件箱的邮箱 — 内容类型混杂、已读状态各异、有会话线程,量也足够大,能把性能问题暴露出来。

压力测试

mailbox-stress.test.js 走得更远:它往一个邮箱里灌 500 封邮件(在全部五种 MIME 类型间轮换),然后测试 MailVault 的 IMAP 层能否正确应付这个量。这能抓出分块 UID FETCH(为避开 IMAP 命令长度限制,我们把获取拆成每批 200 封)、分页,以及高负载下内存占用方面的问题。

第 4 层:真实应用中的端到端测试

最后一层用 WebdriverIO 配合 Tauri WebDriver,驱动真正的 Tauri 桌面应用。应用带 webdriver 特性标志编译,作为一个真实的桌面窗口启动,测试像用户那样与它交互 — 点按钮、往输入框里打字、等邮件加载。

我们把 E2E 测试分成两套。UI 套件(6 个测试文件)测试一切不需要 IMAP 连接的部分:设置面板、侧栏交互、键盘快捷键、布局与视图切换,以及撰写模板。它们跑得快,因为完全不碰网络。

连线套件(11 个测试文件)用真实账户测试完整的应用 — 其中一个账户有 17000 多封邮件。那不是造出来的基准数据;它是一个真实邮箱,攒了好些年的邮件、混杂的 MIME 类型、层层嵌套的会话,以及长期使用的邮箱必然带来的各种脏乱边角情况。拿这样规模的邮箱跑 E2E 测试,能逼着我们在分页、内存占用和大规模下的体感性能上诚实面对自己。这些场景包括:

冷启动 — 应用在没有缓存的情况下启动,添加一个账户,测量邮件出现所需的时间。这是我们持续跟踪的一项基准:从彻底冷的状态到第一批邮件可见。

账户管理 — 添加账户、在账户间切换,确认每个账户各自的状态(上次选中的邮箱、缓存的邮件列表)都被正确保留。

邮件列表与查看器 — 滚动邮件列表、选中邮件、确认正文能正确加载(从缓存、从 Maildir,或者兜底从 IMAP),并检查相邻邮件是否被预取。

撰写、回复与转发 — 打开撰写对话框,回复一封邮件(检查引用正文和收件人是否正确),以及带附件转发时附件是否保留。

搜索 — 拿预置好的邮箱测试搜索栏,验证结果准确性和性能。

文件夹操作 — 在文件夹之间移动邮件、归档、删除(删除用的是 RFC 4315 的 UID EXPUNGE,只移除指定的那封)。

统一收件箱 — 测试把所有账户的邮件合并成单一时间序列表的多账户统一视图。

性能基准 — 冷启动耗时、邮件加载延迟、会话归并性能。它们会输出一张带 p50、p95 和最大值的汇总表,好让我们发现性能回退。

每一个联网的 E2E 测试都有一道安全机制:一份获准的测试账户白名单。如果某个测试不小心去改一个不在白名单里的账户,它会立即失败。这可以避免开发期间的测试代码碰到真实用户数据。

这为什么重要

MailVault 处理的是你的邮件。那是私人的、重要的数据。当你添加账户、切换文件夹、归档一封邮件或搜索收件箱时,它必须能用 — 不只是在一切顺利时,还要在服务器很慢、连接断开、邮件头畸形、OAuth2 令牌在拉取途中过期时也能用。

四层做法让我们在每一层都有底气:Rust 测试验证底层原语,JS 单元测试验证应用逻辑,集成测试验证真实服务器的行为符合预期,E2E 测试验证整套东西作为桌面应用能跑通。我们发布一个版本时,这些全都通过了。