本文へ移動
ブログに戻る

MailVaultのテスト方法: 600以上のテスト、実際のメール、実際のサーバー

6分で読めます

メールは、表面上は単純に見えて実際はまったくそうではないものの一つです。IMAPサーバーはそれぞれ挙動が違います。MIMEのエンコードには、多くの開発者のキャリアより古い例外的な事例があります。OAuth2のトークンは都合の悪いときに期限切れになります。添付ファイルは、multipartのメッセージの中に3段階の入れ子で入っていることもあります。メールクライアントを作るなら、執拗にテストするか、不具合を出荷するかのどちらかです。

MailVaultには現在、4つの層にまたがる600以上の自動テストがあります。Rustのユニットテスト、JavaScriptのユニットテスト、実際のIMAPサーバーに対する統合テスト、そして実際のデスクトップアプリを操作するエンドツーエンドテストです。それらがどう組み合わさっているかを紹介します。

第1層: Rustのユニットテスト(55件)

MailVaultの中核はRustで書かれています — IMAPの接続、SMTPの送信、MIMEの解析、OAuth2のトークン管理はすべて、Node.jsのサイドカーを経由せずネイティブに動作します。Rustの層には、正しく作るのが最も難しい関数を対象とした55件のユニットテストがあります。

良い例がUIDの範囲の圧縮です。MailVaultがUIDの[1, 2, 3, 5, 6, 10]についてヘッダーを取得するとき、コマンドの大きさを抑えるために、これらをIMAPの範囲の文字列"1:3,5:6,10"へ圧縮します。テストは、空の入力、単一のUID、連続する範囲、飛び、並んでいない入力、重複、大きな範囲、そして逆の操作(範囲を個々のUIDへ展開すること)を扱います。compress → expandで元の集合に戻ることを確かめる往復のテストもあります。分割したUID FETCHのコマンドは範囲が誤っていると壊れるので、これらは重要です。

Microsoft Graph APIのモジュールには、GraphのJSONの応答をMailVaultの内部のEmailHeaderの形式へ解析するテストが独自にあり、主となるモジュールのテストは、Maildirのファイル名の解析、フラグのエンコード、UIDによるファイルの検索を扱います。

第2層: JavaScriptのユニットテスト(13スイート)

フロントエンドには、Vitestで動く13のユニットテストのスイートと、ストアの水準のテストがあります。これらは、RustのバックエンドとReactのUIの間にある処理を対象にします。

メールの解析が最も手厚くテストされている領域です。emailParserのスイートは、相手の抽出(「John Doe <[email protected]>」を名前とアドレスに分けること)、件名の正規化(Re:、Fwd:、FW:、Re[N]:のあらゆる組み合わせを取り除くこと)、署名の検出、引用の階層の判別を試します。スレッド化のアルゴリズム — Jamie Zawinskiのスレッド化の仕様を簡略にしたもの — には、In-Reply-Toの連なり、Referencesのヘッダーをたどること、件名による代替のまとめ方、そして壊れたヘッダーの循環の検出についてのテストがあります。

ほかのスイートは、背後のパイプライン(並行数の制御、指数的な待ち時間での再試行、アクティブなアカウントと背後のアカウントの間の順序)、OAuth2のトークンの扱い(期限切れの前の先回りの更新、同時の更新の重複の排除)、差分同期の最適化(4段階のCONDSTOREの判断の木)、素早い読み込みの流れ(キャッシュ済みのヘッダーを即座に表示し、背後の同期が変更を埋めること)、粘り強い読み込み(取得の途中でIMAPの接続が切れたときの緩やかな縮退)、そしてZustandのストアをまたぐ状態の遷移をテストします。

ストアのテスト自体は、LRUキャッシュの追い出し(MailVaultはメールの本文を、上限を設定できるメモリ上のキャッシュに置きます)、フィールドの除去(メモリを節約するため、キャッシュに入れる前にrawSourceと添付ファイルの中身を取り除くこと)、キャッシュの大きさの見積もりを確かめます。

第3層: 実際のIMAPサーバーに対する統合テスト

モックを使ったユニットテストで分かることには限りがあります。MailVaultの統合テストは、実際の認証情報で実際のIMAPサーバーに接続し、実際のメールを送信します。専用のドメインに2つのテスト用アカウントを用意していて、統合テストを走らせるたびにTLSでそれらのアカウントと通信します。

中心となるemail.test.jsのスイートは、一連の流れをまるごと実行します。IMAPで接続し、Nodemailerを使ってSMTPでメールを送り、届くのを待ち、mailparserで取得して解析し、ヘッダーと本文を確かめ、後片付けでテスト用のメッセージを削除します。これにより、モックでは見逃す現実の問題を捕まえられます — TLSのやり取りの癖、サーバーごとのIMAPの拡張、エンコードの違いなどです。

IMAPの最適化のスイートは、MailVaultの高度なプロトコルの機能を実際のサーバーに対してテストします。COMPRESS=DEFLATEのやり取り(サーバーは本当に対応しているか。圧縮したセッションは動くか)、CONDSTOREの差分同期(CHANGEDSINCEは正しい変更後のフラグを返すか)、そしてESEARCHの簡潔な応答です。

アカウントをまたぐメールのスイートは、複数のアカウントの場面をテストします。アカウントAからアカウントBへ送り、メッセージがBの受信トレイに現れることを確かめ、既読と未読の状態の伝わり方を試し、スレッド化のアルゴリズムが両方のアカウントにまたがるやり取りを正しく結ぶことを確認します。

500通のシード

現実に近い条件でテストするために、テスト用のメールボックスに500通のメールを投入しています — 2つのアカウントの間で、それぞれの向きに250通ずつです。シード用のスクリプトは、現実味のある件名(「Q4 Revenue Report — Final Numbers」「Re: Bug in checkout flow」「Invoice #4821 — Due January 15」)でメールを生成し、5種類のMIMEタイプを順に使います。プレーンテキスト、HTML、multipart/alternative、添付ファイルが1つのHTML、そして複数の添付ファイル(CSV、JSON、PNG)が付いたHTMLです。

投入したメールのおよそ3分の1は未読にしてあります。添付ファイルには、生成したPDFのレポート、CSVのデータファイル、JSONのメタデータ、PNGの画像が含まれます。これによって、本物の受信トレイのように見え、そのように振る舞うメールボックスが手に入ります — 内容の種類が混ざり、既読の状態がばらつき、会話がスレッド化され、性能の問題が表に出るだけの量があります。

ストレステスト

mailbox-stress.test.jsのスイートはさらに先へ進みます。1つのメールボックスに500通のメールを投入し(5種類のMIMEタイプを順に使います)、MailVaultのIMAPの層がその量を正しく扱えるかをテストします。これにより、分割したUID FETCH(IMAPのコマンドの長さの上限を避けるため、取得を200件ずつに分けています)、ページ送り、負荷のもとでのメモリの使用にまつわる問題を捕まえられます。

第4層: 実際のアプリでのエンドツーエンドテスト

最後の層は、Tauri WebDriverとWebdriverIOを使って、実際のTauriのデスクトップアプリを操作します。アプリはwebdriverのフィーチャーフラグ付きでコンパイルされ、本物のデスクトップのウィンドウとして起動され、テストは利用者と同じようにそれを操作します — ボタンを押し、欄に入力し、メールが読み込まれるのを待つのです。

E2Eのテストは2つのスイートに分けています。UIのスイート(6つのテストのファイル)は、IMAPの接続を必要としないものすべてをテストします。設定のパネル、サイドバーの操作、キーボードショートカット、レイアウトと表示の切り替え、作成のテンプレートです。ネットワークに触れないので速く走ります。

接続ありのスイート(11のテストのファイル)は、実際のアカウントでアプリ全体をテストします — うち1つは17,000通を超えるメールを持つアカウントです。作り物のベンチマークではなく、何年も積み重なったメッセージ、混ざったMIMEタイプ、入れ子になったスレッド、そして長く使われたメールアカウントに付き物の厄介な端の場合を抱えた、本物のメールボックスです。その大きさのメールボックスに対してE2Eのテストを走らせることで、ページ送り、メモリの使用、そして規模が大きいときの体感の速さについて、私たちは正直でいられます。こうした場面には次のものが含まれます。

コールドスタート — キャッシュのない状態でアプリを起動し、アカウントを追加して、メールが現れるまでの時間を測ります。これは私たちが追っている指標です。完全に冷えた状態から最初のメールが見えるまでの時間です。

アカウントの管理 — アカウントの追加、切り替え、そしてアカウントごとの状態(最後に選んだメールボックス、キャッシュ済みのメールの一覧)が正しく残ることの確認です。

メールの一覧と表示 — メールの一覧をスクロールし、メールを選び、本文が正しく読み込まれること(キャッシュから、Maildirから、あるいは代替としてIMAPから)を確かめ、前後のメールが先読みされているかを見ます。

作成、返信、転送 — 作成のモーダルを開き、メールに返信し(引用された本文と宛先が正しいかを確認)、添付ファイルを保ったまま転送します。

検索 — 投入したメールボックスに対して検索バーを試し、結果の正確さと速さを確かめます。

フォルダの操作 — フォルダ間でのメールの移動、アーカイブ、削除(対象のメッセージだけを消すためにRFC 4315のUID EXPUNGEを使います)です。

統合受信トレイ — すべてのアカウントのメールを1つの時系列の一覧にまとめる、複数アカウントの統合の表示をテストします。

性能の指標 — コールドスタートの時間、メールの読み込みの待ち時間、スレッド化の速さです。p50、p95、最大の時間をまとめた表を出力するので、性能の後退に気づけます。

接続ありのE2Eテストにはすべて、安全のための仕組みがあります。承認済みのテスト用アカウントの許可リストです。テストが誤って許可リストにないアカウントを変更しようとすると、その場で失敗します。これにより、開発中にテストのコードが実際のユーザーのデータに触れることはありません。

なぜこれが大事なのか

MailVaultはあなたのメールを扱います。個人的で、大切なデータです。アカウントを追加し、フォルダを切り替え、メッセージをアーカイブし、受信トレイを検索するとき、それは動かなければなりません — うまくいく道筋だけでなく、サーバーが遅いとき、接続が切れたとき、メールのヘッダーが壊れているとき、取得の途中でOAuth2のトークンが期限切れになったときにも、です。

4層のやり方は、それぞれの水準で確信を与えてくれます。Rustのテストは低水準の部品を検証し、JavaScriptのユニットテストはアプリケーションのロジックを検証し、統合テストは実際のサーバーが期待どおりに振る舞うことを検証し、E2Eテストは全体がデスクトップアプリとして動くことを検証します。リリースを出すときには、これらのすべてが通っています。