昔Advent Calendarのために書いた記事をvue-next版に書き直しました。
https://qiita.com/jiko21/items/12c79b7b831276e9a088
TL;DR
- composition-apiを使うことでロジックをUIから分離してテストできる
- ただし、少し考えてテストを書く必要あり
はじめに
9/18にVue 3.0がリリースされました!
これにより、composition-api
がプラグイン無しで利用できるようになりました。
composition-api
の詳しい使い方はここでは省略しますが、これにより、コンポーネントからロジックを別ファイルへと分離することもできます。
今回は、コンポーネントからロジックを分離した際のテストの書き方を説明します。
コードはすべてこちらにあります。
ロジックとUIの分離
例えば、TODOアプリの場合は、TODOリストに追加、削除する処理や、TODOリストのデータなどをComponentに直接記述するのではなく、
あくまで別のファイルに記述し、Component側でそれらを呼び出す、といったことがComposition-apiでは可能となります。
import { computed, reactive } from 'vue';
const useTodo = () => {
const todo = reactive({
todos: [] as string[],
length: computed(() => todo.todos.length),
}) as any;
const addTodo = (item: string) => {
todo.todos.push(item);
};
const deleteTodo = (index: number) => {
todo.todos.splice(index, 1);
};
return {
todo,
addTodo,
deleteTodo,
};
};
export default useTodo;
<template>
<div class="count">
<input id="todo-input" v-model="text"/>
<button class="add-btn" @click="onSubmit">追加</button>
<ul>
<li v-for="(task, i) in todo.todos" :key="i">
<p>{{task}}</p>
<button class="delete-btn" @click="deleteTodo(i)">Delete</button>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import useTodo from '@/composable/todo';
export default defineComponent({
name: 'Todo',
setup() {
const text = ref('');
const { todo, addTodo, deleteTodo } = useTodo(); // 外部ファイルに書いたロジックを読み込む
...
},
});
</script>
<style scoped>
</style>
テストの実装
このようにロジック(composable)とUIを分離したので次はテストの実装方針を説明します。
まずは、ロジック側からですが、
実際にモジュールとして正しく動くか
を検証します。(当たり前っちゃ当たり前かもですが)
そして、UI側では
ボタンタップ時にロジックをcallできているか
を検証します。
ロジック側のテスト
サービス層やutilで書いたテストコードと同様に、実際に呼び出しを行い検証を行います。
(もしcomosableに何らかの依存関係がある場合はそれをmockしてください)
import useTodo from '@/composable/todo';
describe('todo.spec.ts', () => {
it('addTodo should work properly', () => {
const { todo, addTodo, deleteTodo } = useTodo();
addTodo('hogehoge');
expect(todo.todos).toEqual(['hogehoge']);
});
...
});
UI側のテスト
ロジック(composable)をモックしてやり、それが呼ばれたかを検証します。
compsable内にstateがある場合はそれが表示されるかも確認しておきましょう。
import { mount, VueWrapper } from '@vue/test-utils';
import Todo from '@/components/Todo.vue';
import * as composable from '@/composable/todo';
describe('Todo.vue', () => {
let wrapper: VueWrapper<any>;
let addTodoMock: jest.Mock;
let deleteTodoMock: jest.Mock;
beforeEach(() => {
jest.mock('@/composable/todo');
addTodoMock = jest.fn();
deleteTodoMock = jest.fn();
const TODOS = [
'アドベントカレンダー',
'修論',
'筋トレ',
];
jest.spyOn(composable, 'default').mockReturnValue({
// Reactiveはデータ構造そのままでOK!
todo: {
todos: TODOS,
length: () => TODOS.length,
},
addTodo: addTodoMock,
deleteTodo: deleteTodoMock,
});
wrapper = mount(Todo);
});
it('correctly renders initial html', () => {
expect(wrapper.html()).toMatchSnapshot();
});
it('correctly call deleteTodo when `Delete` button is clicked', () => {
const INDEX = 1;
wrapper.findAll('.delete-btn')[INDEX].trigger('click');
expect(deleteTodoMock).toHaveBeenCalledWith(INDEX);
});
...
});
ここで重要なのは、Reactiveのモック方法です。
データ構造そのままのObjectをmockしてやればできます。
これはRefも同様で、
const countValue = ref(0);
countValue.value;
のように使用するので
jest.spyOn(composable, 'default').mockReturnValue({
countValue: {
value: 0,
},
increment: incrementMock,
decrement: decrementMock,
});
と書いてしまいたくなりますが
jest.spyOn(composable, 'default').mockReturnValue({
countValue: 0 as any,
increment: incrementMock,
decrement: decrementMock,
});
wrapper = mount(Count);
のように書いてやる必要があります。
最後に
このようにすれば、composableとしてロジックを分離した場合でもテストがかけます。
ただし、vue-test-utilsが9/28時点でまだ2.0.0-beta.5
なのでまだまだ安定しないかもしれません。