次のVitestのテストで、モック関数logが 'start' → 'processing' → 'done' の順で3回呼ばれた後に expect(log).toHaveBeenCalledWith('start') を実行した場合、アサーションの結果はどうなりますか?
解説
正解は「パスする」です。toHaveBeenCalledWithは、モック関数がこれまでに一度でも指定した引数で呼ばれたかを検証するマッチャーで、最後の呼び出しだけを見ているわけではありません。logは3回呼ばれていますが、そのうち1回目が 'start' なので条件を満たします。「最後の呼び出しを検証する」のはtoHaveBeenLastCalledWithの挙動なので混同しやすいポイントです。「全ての呼び出しが一致する必要がある」もよくある誤解ですが、そもそもそういう挙動のビルトインマッチャーは存在しません。呼び出しの何回目を検証したいかで使い分ける「何回目の呼び出しか」をピンポイントでチェックしたい場合、Vitestはいくつかのバリエーションを用意しています。順序が意味を持つ処理をテストするときは、次のように使い分けるのが定石です。toHaveBeenCalledWith(args): いずれかの呼び出しがargsに一致すればパスtoHaveBeenLastCalledWith(args): 最後の呼び出しがargsに一致すればパスtoHaveBeenNthCalledWith(n, args): n回目の呼び出しがargsに一致すればパス(nは1始まり)オブジェクト引数はディープイコールで比較されるもうひとつ押さえておきたいのが、引数の比較ルールです。toHaveBeenCalledWithはオブジェクトや配列を参照ではなく構造(中身)で比較します。つまり次のコードはパスします。const fn = vi.fn() fn({ id: 1, name: 'Alice' }) expect(fn).toHaveBeenCalledWith({ id: 1, name: 'Alice' })一部のプロパティだけ検証したい、あるいは毎回変わるIDや日付を無視したい場合は、expect.objectContainingやexpect.any(Number)といった非対称マッチャーを組み合わせます。これを知っているかどうかで「動的な値を含む関数呼び出し」のテストの書きやすさが大きく変わります。