JestでTDDを実践しながらNuxt.jsで簡単なToDoリストをつくってみました。
目次
- ToDoリストのストーリーを考える
- 「ユーザーはフォームと作成ボタンを見ることができる」のテストを書く
- 「ユーザーはフォームと作成ボタンを見ることができる」を実装する
- 「ユーザーはフォームと作成ボタンを見ることができる」をテストする
- 「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書く
- 「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」を実装する
- 「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」をテストする
- 「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」のテストを書く
- 「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」を実装する
- 「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」をテストする
- 「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」のテストを書く
- 「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」を実装する
- 「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」をテストする
- 「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストを書く
- 「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」を実装する
- 「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストをする
- 「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストを書く
- 「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」を実装する
- 「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストをする
- 全部のテストを流す
ToDoリストのストーリーを考える
Nuxt.jsで簡単なToDoリストをつくりにあたり、ユーザーストーリーを考えます。考えた結果、以下のストーリーができました。
- ユーザーはフォームと作成ボタンを見ることができる
- ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる
- ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる
- ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない
- ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる
- ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる
ToDoリストの画面は、TaskFormコンポーネントとTaskListコンポーネントに分けて実装します。
またタスクのデータはFirestoreに格納するようにし、REST APIでデータを操作します。
Nuxt.jsやJestのインストールやFirestoreのセットアップなどは省略しています。
「ユーザーはフォームと作成ボタンを見ることができる」のテストを書く
まず「ユーザーはフォームと作成ボタンを見ることができる」のテストを書いていきます。
TaskFormコンポーネントを作成し、タスク入力フォームと作成ボタンが存在することをチェックします。
・components/TaskForm.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import { mount } from '@vue/test-utils' import TaskForm from '@/components/TaskForm.vue' describe('components/TaskForm.vue', () => { let wrapper beforeEach(() => { wrapper = mount(TaskForm) }) describe('template', () => { test('タスク入力フォームが存在すること', () => { const form = wrapper.find('input'); expect(form.exists()).toBe(true); }) test('タスク作成ボタンが存在すること', () => { const button = wrapper.find('input'); expect(button.exists()).toBe(true); }) }) }) |
テストを作成したら実際にテストを実行してみます。TDDでいうレッドのところです。
まだ実装はしていないので全部のテストが失敗しますが、期待通りの落ち方をしているかを確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | FAIL components/TaskForm.spec.js components/TaskForm.vue template ✕ タスク入力フォームが存在すること (10 ms) ✕ タスク作成ボタンが存在すること (1 ms) ● components/TaskForm.vue > template > タスク入力フォームが存在すること expect(received).toBe(expected) // Object.is equality Expected: true Received: false 16 | test('タスク入力フォームが存在すること', () => { 17 | const form = wrapper.find('input'); > 18 | expect(form.exists()).toBe(true); | ^ 19 | }) 20 | 21 | test('タスク作成ボタンが存在すること', () => { at Object.<anonymous> (components/TaskForm.spec.js:18:29) ● components/TaskForm.vue > template > タスク作成ボタンが存在すること expect(received).toBe(expected) // Object.is equality Expected: true Received: false 21 | test('タスク作成ボタンが存在すること', () => { 22 | const button = wrapper.find('input'); > 23 | expect(button.exists()).toBe(true); | ^ 24 | }) 25 | 26 | }) at Object.<anonymous> (components/TaskForm.spec.js:23:31) |
存在していない、実装されていないなどの落ち方をしていればひとまず完了です。
「ユーザーはフォームと作成ボタンを見ることができる」を実装する
テストで書いたシナリオを元にTaskFormコンポーネントを実装してきます。今回は簡易的なHTMLでフォームとボタンを作成しました。
・components/TaskForm.vue
1 2 3 4 5 6 7 8 | <template> <div> <input> <button> タスク作成 </button> </div> </template> |
「ユーザーはフォームと作成ボタンを見ることができる」をテストする
実装が終わったら、再度TaskFormのテストを流してみます。TDDでいうグリーンのところです。
テストが全て通ったらこのストーリーの実装は完了です。必要に応じてリファクタリングをします。
1 2 3 4 5 | PASS components/TaskForm.spec.js components/TaskForm.vue template ✓ タスク入力フォームが存在すること (9 ms) ✓ タスク作成ボタンが存在すること (1 ms) |
「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書く
次に「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」のテストを書いていきます。
TaskListコンポーネントでToDoリストと完了リストを表示するようにします。
そのため、ToDoリストと完了リストにFirestoreに格納されているデータをfetchしてくるようにします。
・components/TaskList.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | import Vuex from 'vuex' import { mount, createLocalVue} from '@vue/test-utils' import * as indexStore from '@/store' import TaskList from '@/components/TaskList.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('components/TaskList.vue', () => { let wrapper let store let todoTask let doneTask beforeEach(() => { store = new Vuex.Store(indexStore) todoTask = { id: '1', content: 'content_1', status: 'todo' } doneTask = { id: '2', content: 'content_2', status: 'done' } wrapper = mount(TaskList, { store: store, localVue }) store.replaceState({ tasks: [todoTask, doneTask] }) }) describe('template', () => { test('todoリストが表示されること', () => { const li = wrapper.find('li.todo') expect(li.find('span').text()).toBe(todoTask.content) expect(li.find('button).text()).toBe('完了') }) test('完了リストが表示されること', () => { const li = wrapper.find('li.done') expect(li.find('span').text()).toBe(doneTask.content) expect(li.find('button).text()).toBe('ToDoに戻す') }) }) describe('script', () => { describe('computed', () => { describe('todos', () => { test('storeからtodoTasksが取得できること', () => { expect(wrapper.vm.todoTasks).toEqual(expect.arrayContaining([todoTask])) }) test('storeからdoneTasksが取得できること', () => { expect(wrapper.vm.doneTasks).toEqual(expect.arrayContaining([doneTask])) }) }) }) }) }) |
タスクリストの状態管理をするストアのテストを書きます。
ステータスの状態によってToDoタスクと完了タスクを出し分けるようにします。
Storeではaxiosを使って外部サービスにHTTPリクエストしているためaxiosをモック化してテストしています。
async/awaitでaxiosの非同期処理を実現し、結果をgettersから取り出し比較します。
参考:https://github.com/nuxt-community/axios-module/issues/105
・store/index.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | import Vuex from 'vuex' import * as index from '@/store' import { createLocalVue } from '@vue/test-utils' import _ from 'lodash' import axios from 'axios' const localVue = createLocalVue() localVue.use(Vuex) let mockAxiosGetResult jest.mock('axios', () => ({ get: jest.fn(() => Promise.resolve(mockAxiosGetResult)) })) let action const testedAction = (context = {}, payload = {}) => { return index.actions[action].bind({ $axios: axios })(context, payload) } describe('store/index.js', () => { let store let todoTask, doneTask beforeEach(() => { store = new Vuex.Store(_.cloneDeep(index)) todoTask = { id: '1', content: 'content_1', status: 'todo' } doneTask = { id: '2', content: 'content_2', status: 'done' } }) describe('getters', () => { let tasks let todoTasks let doneTasks beforeEach(() => { tasks = [todoTask, doneTask] todoTasks = [todoTask] doneTasks = [doneTask] store.replaceState({ tasks, todoTasks, doneTasks }) }) describe('todoTasks', () => { test('statusがtodoのtaskが取得できること', () => { expect(store.getters['todoTasks']).toContainEqual(todoTask) expect(store.getters['todoTasks']).not.toContainEqual(doneTask) }) }) describe('doneTasks', () => { test('statusがdoneのtaskが取得できること', () => { expect(store.getters['doneTasks']).not.toContainEqual(todoTask) expect(store.getters['doneTasks']).toContainEqual(doneTask) }) }) describe('tasks', () => { test('すべてのtasksが取得できること', () => { expect(store.getters['tasks']).toEqual( expect.arrayContaining(tasks) ) }) }) }) describe('actions', () => { let commit beforeEach(() => { commit = store.commit }) describe('fetchTasks', () => { test('tasksが取得できること', async () => { action = 'fetchTasks' mockAxiosGetResult = { data: { documents: [ { name: `tasks/${todoTask.id}`, fields: { content: { stringValue: todoTask.content }, status: { stringValue: todoTask.status } } }, { name: `tasks/${doneTask.id}`, fields: { content: { stringValue: doneTask.content }, status: { stringValue: doneTask.status } } } ] } } await testedAction({ commit }) expect(store.getters['tasks']).toEqual([todoTask, doneTask]) }) }) }) }) |
テスト結果を省略していますが、ここでまたテストを実行し期待通りに落ちているかを確認します。
「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」を実装する
テストが書いたシナリオを元にTaskListコンポーネントとindexページ、indexストアを実装してきます。
TaskListコンポーネントではToDoリストと完了リストがあり、indexページではTaskListコンポーネントとTaskFormコンポーネントを呼び出してタスクをストアから呼び出すようにします。
indexストアではFirestoreからデータを取ってきてToDoタスクと完了タスクに状態管理するようにします。
・components/TaskList.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <template> <div> <p>ToDoリスト</p> <ul> <li v-for="todoTask in todoTasks" :key="todoTask.id" class="todo" > <span>{{ todoTask.content }}</span> </li> </ul> <hr> <p>完了リスト</p> <ul> <li v-for="doneTask in doneTasks" :key="doneTask.id" class="done" > <span>{{ doneTask.content }}</span> </li> </ul> </div> </template> <script> import { mapGetters } from 'vuex' import _ from 'lodash' export default { computed: { ...mapGetters(['todoTasks']), ...mapGetters(['doneTasks']) } } </script> |
・pages/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <template> <section> <TaskForm/> <TaskList/> </section> </template> <script> import TaskForm from '@/components/TaskForm.vue' import TaskList from '@/components/TaskList.vue' export default { async asyncData({ store }) { await store.dispatch('fetchTasks') }, components: { TaskForm, TaskList } } </script> |
・store/index.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import _ from 'lodash' export const state = () => ({ tasks: [] }) export const getters = { tasks: state => state.tasks, todoTasks: state => state.tasks.filter(task => task.status === 'todo'), doneTasks: state => state.tasks.filter(task => task.status === 'done') } export const mutations = { updateTasks(state, tasks) { state.tasks = tasks } } export const actions = { async fetchTasks({ commit }) { await this.$axios.get(`/tasks`, { baseURL: process.env._AXIOS_BASE_URL_ }) .then(res => { let tasks = [] if (_.has(res.data, 'documents')) { tasks = res.data.documents.map(doc => { return { id: _.last(doc.name.split('/')), content: doc.fields.content.stringValue, status: doc.fields.status.stringValue } }) } commit('updateTasks', tasks) }) } } |
「ユーザーはToDoタスク一覧と完了タスク一覧を見ることができる」をテストする
実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。
1 2 3 4 5 6 7 8 9 10 | PASS components/TaskList.spec.js components/TaskList.vue template ✓ todoリストが表示されること (13 ms) ✓ 完了リストが表示されること (2 ms) script computed todos ✓ storeからtodoTasksが取得できること (2 ms) ✓ storeからdoneTasksが取得できること (1 ms) |
1 2 3 4 5 6 7 8 9 10 11 12 | PASS store/index.spec.js store/index.js getters todoTasks ✓ statusがtodoのtaskが取得できること (3 ms) doneTasks ✓ statusがdoneのtaskが取得できること (1 ms) tasks ✓ すべてのtasksが取得できること (1 ms) actions fetchTasks ✓ tasksが取得できること |
「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」のテストを書く
先ほどTaskFormコンポーネントに作成したフォームに入力して作成ボタンを押すとToDoタスク一覧に追加されることが確認できるテストを書きます。
・components/TaskForm.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import Vuex from 'vuex' import { mount, createLocalVue } from '@vue/test-utils' import * as store from '@/store' import TaskForm from '@/components/TaskForm.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('components/TaskForm.vue', () => { let wrapper beforeEach(() => { wrapper = mount(TaskForm, { store: store, localVue }) }) describe('template', () => { ... describe('タスク入力がある場合', () => { beforeEach(() => { const mock = jest.fn() wrapper.vm.addTask = mock wrapper.find('input').setValue('content') }) test('dataに入力が反映されること', () => { expect(wrapper.vm.taskForm.content).toBe('content') }) test('タスク作成ボタンを押下するとaddTaskが呼ばれること', () => { wrapper.find('button').trigger('click') expect(wrapper.vm.addTask).toHaveBeenCalled() }) }) }) describe('script', () => { describe('data', () => { test('dataの構造が正しいこと', () => { expect(wrapper.vm.$data).toHaveProperty('taskForm.content') }) }) }) }) |
ストアのテストでは作成ボタンが押下された際にFirestoreに入力されたタスクのデータが保存されることを確認します。
・store/index.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | import Vuex from 'vuex' import * as index from '@/store' import { createLocalVue } from '@vue/test-utils' import _ from 'lodash' import axios from 'axios' const localVue = createLocalVue() localVue.use(Vuex) let mockAxiosGetResult jest.mock('axios', () => ({ ... post: jest.fn(() => Promise.resolve(mockAxiosGetResult)) })) let action const testedAction = (context = {}, payload = {}) => { return index.actions[action].bind({ $axios: axios })(context, payload) } describe('store/index.js', () => { ... describe('actions', () => { let commit beforeEach(() => { commit = store.commit }) ... describe('createTask', () => { test('taskが追加されること', async () => { mockAxiosGetResult = { data: { name: `tasks/${todoTask.id}`, fields: { content: { stringValue: todoTask.content }, status: { stringValue: todoTask.status } } } } action = 'createTask' await testedAction({ commit }) expect(store.getters['todoTasks']).toContainEqual(todoTask) }) }) }) }) |
「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」を実装する
タスク作成ボタンが押されるとaddTaskが呼び出されるように実装していきます。
addTaskメソッドはストアのcreateTaskを呼び出し、タスクが状態管理されるようにします。
・components/TaskForm.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <template> <div> <input v-model="taskForm.content" > <button @click="addTask"> タスク作成 </button> </div> </template> <script> import _ from 'lodash' export default { data() { return { taskForm: { content: '' } } }, methods: { async addTask() { await this.$store.dispatch('createTask', _.cloneDeep(this.taskForm)) this.taskForm.content = '' } } } </script> |
addTaskから呼びだされるcreateTaskの実装をストアに書いていきます。
・store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | import _ from 'lodash' export const state = () => ({ tasks: [] }) export const getters = { tasks: state => state.tasks, ... } export const mutations = { ... addTask(state, newTask) { state.tasks.push(newTask) } ... } export const actions = { async createTask({ commit }, payload) { const req = { fields: { content: { stringValue: payload.content }, status: { stringValue: 'todo' } } } await this.$axios.post('/tasks', req, { baseURL: process.env._AXIOS_BASE_URL_ }).then(res => { const newTask = { id: _.last(res.data.name.split('/')), content: res.data.fields.content.stringValue, status: res.data.fields.status.stringValue } commit('addTask', newTask) }) } } |
「ユーザーはフォームにタスクを入力して作成ボタンを押すと、ToDoタスク一覧に追加されることを確認できる」をテストする
実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。
1 2 3 4 5 6 7 8 9 10 11 | PASS components/TaskForm.spec.js components/TaskForm.vue template ✓ タスク入力フォームが存在すること (11 ms) ✓ タスク作成ボタンが存在すること (6 ms) タスク入力がある場合 ✓ dataに入力が反映されること (3 ms) ✓ タスク作成ボタンを押下するとaddTaskが呼ばれること (2 ms) script data ✓ dataの構造が正しいこと (1 ms) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | PASS store/index.spec.js store/index.js getters todoTasks ✓ statusがtodoのtaskが取得できること (3 ms) doneTasks ✓ statusがdoneのtaskが取得できること (1 ms) tasks ✓ すべてのtasksが取得できること (1 ms) actions fetchTasks ✓ tasksが取得できること (1 ms) createTask ✓ taskが追加されること (1 ms) |
「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」のテストを書く
フォームにタスクが入力していない場合は、作成ボタンを押すことができないことを確認できるテストを書いていきます。
・components/TaskForm.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import Vuex from 'vuex' import { mount, createLocalVue } from '@vue/test-utils' import * as store from '@/store' import TaskForm from '@/components/TaskForm.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('components/TaskForm.vue', () => { let wrapper beforeEach(() => { wrapper = mount(TaskForm, { store: store, localVue }) }) describe('template', () => { ... describe('タスク入力がない場合', () => { beforeEach(() => { const mock = jest.fn() wrapper.vm.addTask = mock wrapper.find('input').setValue('') }) test('dataに入力が反映されること', () => { expect(wrapper.vm.taskForm.content).toBe('') }) test('タスク作成ボタンを押下するとaddTaskが呼ばれないこと', () => { wrapper.find('button').trigger('click') expect(wrapper.vm.addTask).not.toHaveBeenCalled() }) }) }) ... }) |
「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」を実装する
ボタンにdisabled属性を付与し、computedでフォームの入力を監視してボタンの活性非活性を制御するように実装しました。
・components/TaskForm.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <template> <div> <input v-model="taskForm.content" > <button :disabled="activateSubmit" @click="addTask"> タスク作成 </button> </div> </template> <script> import _ from 'lodash' export default { ... computed: { activateSubmit () { return this.taskForm.content == '' } }, ... } </script> |
「ユーザーはフォームにタスクが入力していない場合は、作成ボタンを押すことができない」をテストする
実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | PASS components/TaskForm.spec.js components/TaskForm.vue template ✓ タスク入力フォームが存在すること (9 ms) ✓ タスク作成ボタンが存在すること (1 ms) タスク入力がある場合 ✓ dataに入力が反映されること (2 ms) ✓ タスク作成ボタンを押下するとaddTaskが呼ばれること (2 ms) タスク入力がない場合 ✓ dataに入力が反映されること (1 ms) ✓ タスク作成ボタンを押下するとaddTaskが呼ばれないこと (1 ms) script data ✓ dataの構造が正しいこと (1 ms) |
「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストを書く
ToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できるテストを書いていきます。
・components/TaskList.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import Vuex from 'vuex' import { mount, createLocalVue } from '@vue/test-utils' import * as indexStore from '@/store' import TaskList from '@/components/TaskList.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('components/TaskList.vue', () => { let wrapper let store let todoTask let doneTask beforeEach(() => { store = new Vuex.Store(indexStore) todoTask = { id: '1', content: 'content_1', status: 'todo' } doneTask = { id: '2', content: 'content_2', status: 'done' } wrapper = mount(TaskList, { store: store, localVue }) store.replaceState({ tasks: [todoTask, doneTask] }) }) describe('template', () => { ... describe('todoリストの完了をクリックする場合', () => { test('doneTaskが指定の引数で呼び出されること', () => { const mock = jest.fn(todoTask => todoTask) wrapper.vm.doneTask = mock wrapper.find('li.todo button').trigger('click') expect(wrapper.vm.doneTask).toHaveBeenCalled() expect(wrapper.vm.doneTask.mock.results[0].value).toBe(todoTask) }) }) }) ... }) |
ストアではタスクのステータスが更新されることが確認できるテストを書いていきます。
・store/index.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | import Vuex from 'vuex' import * as index from '@/store' import { createLocalVue } from '@vue/test-utils' import _ from 'lodash' import axios from 'axios' const localVue = createLocalVue() localVue.use(Vuex) let mockAxiosGetResult jest.mock('axios', () => ({ ... patch: jest.fn(() => Promise.resolve(mockAxiosGetResult)) })) let action const testedAction = (context = {}, payload = {}) => { return index.actions[action].bind({ $axios: axios })(context, payload) } describe('store/index.js', () => { let store let todoTask, doneTask beforeEach(() => { store = new Vuex.Store(_.cloneDeep(index)) todoTask = { id: '1', content: 'content_1', status: 'todo' } doneTask = { id: '2', content: 'content_2', status: 'done' } }) ... describe('actions', () => { let commit beforeEach(() => { commit = store.commit }) ... describe('updateTask', () => { beforeEach(() => { store.replaceState({ tasks: [todoTask] }) }) test('taskが更新されること', async () => { mockAxiosGetResult = { data: { name: `tasks/${todoTask.id}`, fields: { content: { stringValue: 'updatedContent' }, status: { stringValue: 'done' } } } } action = 'updateTask' await testedAction({ commit }) expect(store.getters['doneTasks']).toContainEqual({ id: todoTask.id, content: 'updatedContent', status: 'done' }) }) }) }) }) |
「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」を実装する
ToDoタスクの完了ボタンを押すと、ストアのupdateTaskを呼び出しステータスを完了にすることで完了タスク一覧に移動されるように実装します。
・components/TaskList.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <template> <div> <p>ToDoタスク</p> <ul> <li v-for="todoTask in todoTasks" :key="todoTask.id" class="todo" > <span>{{ todoTask.content }}</span> <button @click="doneTask(todoTask)"> 完了 </button> </li> </ul> <hr> <p>完了タスク</p> <ul> <li v-for="doneTask in doneTasks" :key="doneTask.id" class="done" > <span>{{ doneTask.content }}</span> </li> </ul> </div> </template> <script> import { mapGetters } from 'vuex' import _ from 'lodash' export default { ... methods: { async doneTask(task) { const payload = _.cloneDeep(task) payload.status = 'done' await this.$store.dispatch('updateTask', payload) } } } </script> |
・store/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | import _ from 'lodash' export const state = () => ({ tasks: [] }) export const getters = { tasks: state => state.tasks, ... } export const mutations = { ... updateTask(state, newTask) { const task = state.tasks.find(task => task.id === newTask.id) if (task) { task.content = newTask.content task.status = newTask.status } } } export const actions = { ... async updateTask({ commit }, payload) { const req = { fields: { content: { stringValue: payload.content }, status: { stringValue: payload.status } } } await this.$axios.patch(`/tasks/${payload.id}`, req, { baseURL: process.env._AXIOS_BASE_URL_ }).then(res => { const updateTask = { id: _.last(res.data.name.split('/')), content: res.data.fields.content.stringValue, status: res.data.fields.status.stringValue } commit('updateTask', updateTask) }) } } |
「ユーザーはToDoタスクの完了ボタンを押すと、完了タスク一覧にタスクが移動されることを確認できる」のテストをする
実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。
1 2 3 4 5 6 7 8 9 10 11 12 | PASS components/TaskList.spec.js components/TaskList.vue template ✓ todoリストが表示されること (17 ms) ✓ 完了リストが表示されること (2 ms) todoリストの完了をクリックする場合 ✓ doneTaskが指定の引数で呼び出されること (3 ms) script computed todos ✓ storeからtodoTasksが取得できること (1 ms) ✓ storeからdoneTasksが取得できること (2 ms) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | PASS store/index.spec.js store/index.js getters todoTasks ✓ statusがtodoのtaskが取得できること (3 ms) doneTasks ✓ statusがdoneのtaskが取得できること (1 ms) tasks ✓ すべてのtasksが取得できること actions fetchTasks ✓ tasksが取得できること (1 ms) createTask ✓ taskが追加されること updateTask ✓ taskが更新されること |
「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストを書く
完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できることが確認できるテストを書いていきます。
ストアのタスクステータス更新のテストは先程のストーリーで確認済みなため、新しくストアのテストは書いていません。
・components/TaskList.spec.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | import Vuex from 'vuex' import { mount, createLocalVue } from '@vue/test-utils' import * as indexStore from '@/store' import TaskList from '@/components/TaskList.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('components/TaskList.vue', () => { let wrapper let store let todoTask let doneTask beforeEach(() => { store = new Vuex.Store(indexStore) todoTask = { id: '1', content: 'content_1', status: 'todo' } doneTask = { id: '2', content: 'content_2', status: 'done' } wrapper = mount(TaskList, { store: store, localVue }) store.replaceState({ tasks: [todoTask, doneTask] }) }) describe('template', () => { ... describe('完了リストのToDoに戻すをクリックする場合', () => { test('returnTaskが指定の引数で呼び出されること', () => { const mock = jest.fn(doneTask => doneTask) wrapper.vm.returnTask = mock wrapper.find('li.done button').trigger('click') expect(wrapper.vm.returnTask).toHaveBeenCalled() expect(wrapper.vm.returnTask.mock.results[0].value).toBe(doneTask) }) }) }) ... }) |
「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」を実装する
完了タスクのToDoに戻すボタンを押すと、ストアのupdateTaskを呼び出しステータスをToDoにすることでToDoタスク一覧に移動されるように実装します。
・components/TaskList.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <template> <div> <p>ToDoタスク</p> <ul> <li v-for="todoTask in todoTasks" :key="todoTask.id" class="todo" > <span>{{ todoTask.content }}</span> <button @click="doneTask(todoTask)"> 完了 </button> </li> </ul> <hr> <p>完了タスク</p> <ul> <li v-for="doneTask in doneTasks" :key="doneTask.id" class="done" > <span>{{ doneTask.content }}</span> <button @click="returnTask(doneTask)"> ToDoに戻す </button> </li> </ul> </div> </template> <script> import { mapGetters } from 'vuex' import _ from 'lodash' export default { ... methods: { ... async returnTask(task) { const payload = _.cloneDeep(task) payload.status = 'todo' await this.$store.dispatch('updateTask', payload) } } } </script> |
「ユーザーは完了タスクのToDoに戻すボタンを押すと、ToDoタスク一覧にタスクが移動されることを確認できる」のテストをする
実装が終わったら再度テストを流します。テストが全て通ればこのストーリーは完了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | PASS components/TaskList.spec.js components/TaskList.vue template ✓ todoリストが表示されること (12 ms) ✓ 完了リストが表示されること (3 ms) todoリストの完了をクリックする場合 ✓ doneTaskが指定の引数で呼び出されること (3 ms) 完了リストのToDoに戻すをクリックする場合 ✓ returnTaskが指定の引数で呼び出されること (2 ms) script computed todos ✓ storeからtodoTasksが取得できること (1 ms) ✓ storeからdoneTasksが取得できること (5 ms) |
全部のテストを流す
全部の実装が終わったら、全てのテストを流します。package.jsonのscriptに”test”: “jest –config jest.config.js”を追加し、以下のコマンドで実行します。
1 2 3 4 5 | $ npm run test PASS components/TaskForm.spec.js PASS components/TaskList.spec.js PASS store/index.spec.js |
全てのテストが通ったら完了です。