Vueのコンポーネントのユニットテストでよく書くパターンを紹介します。
環境はvue-cliで生成したものをそのまま使っています。
APIの詳細な説明はVue Test Utilsを参照してください。
スナップショットテスト
出力されるHTMLが予期せず変更されないようにする場合に使うテストです。
it("itemsの値がhtmlに出力されているか?", () => {
const items = [
new Item("アイテム", 3, "アイテム説明")
];
const wrapper = shallowMount(GachaMv, {
propsData: { items }
});
expect(wrapper.html()).toMatchSnapshot();
});
上記のテストを追加しnpm run test:unit
or yarn test:unit
を実行するとテストファイルと同階層に__snapshots__/GachaMv.spec.ts.snap
が出力されます。
exports[`GachaMv.vue itemsの値がhtmlに出力されているか? 1`] = `
<div class="gacha-mv">
<slick-stub options="[object Object]">
<div class="gacha-mv-list">
<p class="gacha-mv-list-name">アイテム</p>
<p class="gacha-mv-list-rare">3</p>
<p class="gacha-mv-list-description">アイテム説明</p>
</div>
</slick-stub>
</div>
`;
shallowMountではなくmountを使用した場合は、以下のように子コンポーネントも展開した状態で出力されます。
exports[`GachaMv.vue itemsの値がhtmlに出力されているか? 1`] = `
<div class="gacha-mv">
<div class="slick-initialized slick-slider">
<div class="slick-list draggable">
<div class="slick-track" style="opacity: 1; width: 0px; transform: translate(0px, 0px);">
<div class="slick-slide slick-current slick-active" data-slick-index="0" aria-hidden="false" style="margin-left: 0px; width: 0px;">
<div>
<div class="gacha-mv-list" style="width: 100%; display: inline-block;">
<p class="gacha-mv-list-name">アイテム名</p>
<p class="gacha-mv-list-rare">3</p>
<p class="gacha-mv-list-description">アイテム説明</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
以下のようにshallowMountとstubsを組み合わせることで、一部のコンポーネントのみスタブすることも可能です。
const wrapper = shallowMount(GachaMv, {
stubs: {
'slick': Slick,
},
}
computedが期待した値を返しているか?
computedが正しい値を確認しているかのテストです。
it("pickupItemが期待通りか?", async() => {
const items = [
new Item("アイテム名3", 3, "アイテム説明3"),
new Item("アイテム名4", 4, "アイテム説明4"),
new Item("アイテム名5", 5, "アイテム説明5"),
];
const wrapper = shallowMount(GachaMv, {
propsData: { items }
});
expect((wrapper.vm as any).pickupItems).toEqual([new Item("アイテム名5", 5, "アイテム説明5")]);
});
クリックイベントが動作しているか?
要素がイベントを発火した時に、定義した関数が実行されているかのテストです。
スタブにsinonを使ってます。
it(".gacha-playクリック時にonPlayGachaが実行されるか?", () => {
const onPlayGachaStub = sinon.stub();
const wrapper = shallowMount(GachaPlay, {
propsData: {
number: 1,
onPlayGacha: onPlayGachaStub
}
});
wrapper.find(".gacha-play").trigger('click');
expect(onPlayGachaStub.called).toBe(true);
onPlayGachaStub.restore();
});
sinon or jest.fn
先ほどのクリックイベントのテストでsinonを使いましたが、jest.fnでも同じことができます。
個人的は使い慣れているsinonを使用しています。
unit testing - stubbing a function using jest - Stack Overflow
非同期のテスト
非同期テストでは、flush-promisesやVue.nextTickを使わないと期待した結果が得られません。
flush-promisesについて
保留中の解決済みの約束のハンドラをすべてフラッシュします。
やっていることはmicrotasks または macrotasksのプロミスを返しているだけです。(setImmediate関数がある場合はmicrotasksが選択されるので、これがDOMの更新を待たなかった原因かもしれません。)
microtasks、macrotasksについは以下の記事が分かりやすかったです。
Tasks, microtasks, queues and schedules - JakeArchibald.com
Vue.nextTickについて
callbackを延期し、DOMの更新サイクル後に実行します。DOM更新を待ち受けるために、いくつかのデータを変更した直後に使用してください。
テスト記述例
VueのDOMの更新後の値をテストする場合はnextTick、非同期処理(Promiseをfulfilled)する場合はflushPromisesを使用します。
it('test', async() => {
const wrapper = shallowMount(Foo);
await flushPromises(); // マウント時などに非同期処理がある場合
expect(wrapper.vm.text).toBe('マウント時のテキスト');
// expect(wrapper.find('text').text()).toBe('マウント時のテキスト'); // DOMが更新される保証はないので、Errorになるかも
wrapper.find('button').trigger('click'); // クリックイベントで非同期処理を実行
await wrapper.vm.$nextTick(); // DOMの更新を保証
expect(wrapper.find('text').text()).toBe('クリック後のテキスト');
})
さいごに
簡単なコンポーネントのテストであれば、これらの組み合わせで十分かと思います。
今回のユニットテストではUIの崩れなど検知できないので、以下のリンク先で紹介されているようなstorybookやvisual regressionテストなどの導入を検討してみてください。
Storybookとvue-i18nで多言語確認を容易にしよう - スタディスト開発ブログ - Medium
Storybookとreg-suitで気軽にはじめるVisual Regression Testing - wadackel.me