スナップショット
Vue School の動画でスナップショットを学ぶスナップショットテストは、関数の出力が予期せず変更されないことを確認する際に非常に役立つツールです。
スナップショットテストでは、Vitest は指定された値のスナップショットを作成し、それをテストと一緒に保存されている参照スナップショットファイルと比較します。2つのスナップショットが一致しない場合、テストは失敗します。これは、変更が予期しないものであるか、参照スナップショットを新しい結果のバージョンに更新する必要があるかのいずれかです。
スナップショットの使用
値をスナップショットするには、expect()
API の toMatchSnapshot()
を使用できます。
import { expect, it } from 'vitest';
it('toUpperCase', () => {
const result = toUpperCase('foobar');
expect(result).toMatchSnapshot();
});
このテストが最初に実行されると、Vitest は次のようなスナップショットファイルを作成します。
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports['toUpperCase 1'] = '"FOOBAR"';
スナップショットファイルはコード変更と一緒にコミットし、コードレビュープロセスの一部としてレビューする必要があります。その後のテスト実行では、Vitest はレンダリングされた出力を以前のスナップショットと比較します。一致すればテストは合格します。一致しない場合、テストランナーがコードのバグを発見したか、実装が変更されスナップショットを更新する必要があるかのいずれかです。
WARNING
非同期の並行テストでスナップショットを使用する場合、正しいテストが検出されるように、ローカルのテストコンテキストにある expect
を使用する必要があります。
インラインスナップショット
同様に、toMatchInlineSnapshot()
を使用して、スナップショットをテストファイル内にインラインで保存できます。
import { expect, it } from 'vitest';
it('toUpperCase', () => {
const result = toUpperCase('foobar');
expect(result).toMatchInlineSnapshot();
});
スナップショットファイルを作成する代わりに、Vitest はテストファイルを直接変更して、スナップショットを文字列として更新します。
import { expect, it } from 'vitest';
it('toUpperCase', () => {
const result = toUpperCase('foobar');
expect(result).toMatchInlineSnapshot('"FOOBAR"');
});
これにより、複数のファイルを切り替えることなく、期待される出力を直接確認できます。
WARNING
非同期の並行テストでスナップショットを使用する場合、正しいテストが検出されるように、ローカルのテストコンテキストにある expect
を使用する必要があります。
スナップショットの更新
受け取った値がスナップショットと一致しない場合、テストは失敗し、両者の差分が表示されます。スナップショットの変更が予期される場合、現在の状態に基づいてスナップショットを更新することができます。
ウォッチモード中は、ターミナルで u
キーを押すと、失敗したスナップショットを直接更新できます。
または、CLI で --update
または -u
フラグを使用すると、Vitest にスナップショットを更新させることができます。
vitest -u
ファイルスナップショット
toMatchSnapshot()
を呼び出すと、すべてのスナップショットはフォーマットされたスナップファイルに保存されます。つまり、スナップショット文字列内では、二重引用符 "
やバッククォート `
といった一部の文字をエスケープする必要があります。また、スナップショット内容のシンタックスハイライト(特定のプログラミング言語の場合)が失われる可能性があります。
このため、明示的にファイルと一致させるために toMatchFileSnapshot()
を導入しました。これにより、スナップショットファイルに任意のファイル拡張子を割り当てることができ、可読性が向上します。
import { expect, it } from 'vitest';
it('render basic', async () => {
const result = renderHTML(h('div', { class: 'foo' }));
await expect(result).toMatchFileSnapshot('./test/basic.output.html');
});
これは ./test/basic.output.html
の内容と比較されます。そして、--update
フラグで更新できます。
画像スナップショット
jest-image-snapshot
を使用して画像をスナップショットすることも可能です。
npm i -D jest-image-snapshot
test('image snapshot', () => {
expect(readFileSync('./test/stubs/input-image.png')).toMatchImageSnapshot();
});
カスタムシリアライザ
スナップショットのシリアライズ方法を変更するための独自のロジックを追加できます。Jest と同様に、Vitest には組み込みの JavaScript 型、HTML 要素、ImmutableJS、そして React 要素に対応するデフォルトシリアライザがあります。
expect.addSnapshotSerializer
API を使用して、カスタムシリアライザを明示的に追加することが可能です。
expect.addSnapshotSerializer({
serialize(val, config, indentation, depth, refs, printer) {
// `printer` は既存のプラグインを使用して値をシリアライズする関数です。
return `Pretty foo: ${printer(val.foo, config, indentation, depth, refs)}`;
},
test(val) {
return val && Object.prototype.hasOwnProperty.call(val, 'foo');
},
});
また、カスタムシリアライザを暗黙的に追加する snapshotSerializers オプションもサポートしています。
import { SnapshotSerializer } from 'vitest';
export default {
serialize(val, config, indentation, depth, refs, printer) {
// `printer` は既存のプラグインを使用して値をシリアライズする関数です。
return `Pretty foo: ${printer(val.foo, config, indentation, depth, refs)}`;
},
test(val) {
return val && Object.prototype.hasOwnProperty.call(val, 'foo');
},
} satisfies SnapshotSerializer;
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
snapshotSerializers: ['path/to/custom-serializer.ts'],
},
});
次のようなテストを追加した後:
test('foo snapshot test', () => {
const bar = {
foo: {
x: 1,
y: 2,
},
};
expect(bar).toMatchSnapshot();
});
以下のスナップショットが出力されます。
Pretty foo: Object {
"x": 1,
"y": 2,
}
スナップショットのシリアライズには Jest の pretty-format
を使用しています。詳細については、こちらをご覧ください: pretty-format。
Jest との違い
Vitest は、いくつかの例外を除き、Jest のスナップショット機能とほぼ互換性のあるスナップショット機能を提供します。
1. スナップショットファイルのコメントヘッダーが異なります {#_1-comment-header-in-the-snapshot-file-is-different}
- // Jest Snapshot v1, https://goo.gl/fbAQLP
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
これは機能自体には影響しませんが、Jest から移行する際にコミットの差分に影響を与える可能性があります。
2. printBasicPrototype
のデフォルトが false
になっています {#_2-printbasicprototype-is-default-to-false}
Jest と Vitest の両方のスナップショットは pretty-format
を基盤としています。Vitest では、よりクリーンなスナップショット出力を提供するため、printBasicPrototype
はデフォルトで false
に設定されていますが、Jest <29.0.0 ではデフォルトで true
です。
import { expect, test } from 'vitest';
test('snapshot', () => {
const bar = [
{
foo: 'bar',
},
];
// Jest の場合
expect(bar).toMatchInlineSnapshot(`
Array [
Object {
"foo": "bar",
},
]
`);
// Vitest の場合
expect(bar).toMatchInlineSnapshot(`
[
{
"foo": "bar",
},
]
`);
});
これは可読性と全体的な開発者体験(DX)にとって、より合理的なデフォルトであると私たちは考えています。Jest と同じ動作を希望する場合は、設定を変更できます。
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
snapshotFormat: {
printBasicPrototype: true,
},
},
});
3. カスタムメッセージの区切り文字としてコロン :
の代わりに山形記号 >
が使用されます {#_3-chevron-is-used-as-a-separator-instead-of-colon-for-custom-messages}
Vitest は、スナップショットファイルの作成時にカスタムメッセージが渡される場合、可読性向上のためコロン :
の代わりに山形記号 >
を区切り文字として使用します。
以下のテストコードの例では:
test('toThrowErrorMatchingSnapshot', () => {
expect(() => {
throw new Error('error');
}).toThrowErrorMatchingSnapshot('hint');
});
Jest では、スナップショットは以下のようになります。
exports[`toThrowErrorMatchingSnapshot: hint 1`] = `"error"`;
Vitest では、対応するスナップショットは以下のようになります。
exports[`toThrowErrorMatchingSnapshot > hint 1`] = `[Error: error]`;
4. toThrowErrorMatchingSnapshot
および toThrowErrorMatchingInlineSnapshot
のデフォルトの Error
スナップショットが異なります {#_4-default-error-snapshot-is-different-for-tothrowerrormatchingsnapshot-and-tothrowerrormatchinginlinesnapshot}
import { expect, test } from 'vitest'
test('snapshot', () => {
// Jest と Vitest の両方で
expect(new Error('error')).toMatchInlineSnapshot(`[Error: error]`)
// Jest は `Error` インスタンスの `Error.message` をスナップショットします
// Vitest は `toMatchInlineSnapshot` と同じ値を出力します
expect(() => {
throw new Error('error')
}).toThrowErrorMatchingInlineSnapshot(`"error"`)
}).toThrowErrorMatchingInlineSnapshot(`[Error: error]`)
})