はじめに
こんにちは。GameWith のエンジニアの tiwu です!
このブログはアドベントカレンダーの20日目のブログになります!
今回は Vue.js, Typescript で開発しているダッシュボードのテストコードについて書いていきたいと思います!
導入についてはこちらの記事を御覧ください。
サンプルページ
サンプルページのコードと共に解説していこうと思います。
例としてユーザーの一覧と検索機能を持ったダッシュボードの場合、下記のような構成になります。
- src - components - pages - user - list.vue - composition - user - use-list.ts
composition
フォルダには Composition API RFC を参考にロジック部分を分けています。
src/components/pages/user/list.vue
検索機能とユーザー一覧を表示するページのサンプルコードです。
コンポーネントライブラリとして BootstrapVue
を利用しています 👍
表示するデータや取得処理は Composition API
経由で、検索は URL パラメータを書き換え更新することで検索を実現させています。
The サンプルコードと言った感じのシンプルな作りになっています。
<template> <div> <label for="input-userId">ユーザID</label> <b-form-input id="input-userId" type="number" :state="userIdModel.state" v-model.number="userIdModel.input" > </b-form-input> <b-button @click="submitSearch" variant="outline-primary">検索</b-button> <b-button @click="resetSearch" variant="outline-secondary">リセット</b-button> <b-pagination-nav :link-gen="linkGen" :number-of-pages="numberOfPage"></b-pagination-nav> <b-table :items="users"> // 省略 </b-table> </div> </template> <script lang="ts"> import { defineComponent, reactive, toRefs, getCurrentInstance } from '@vue/composition-api'; import { useList } from '@/composition/user/use-list'; export default defineComponent({ setup() { const root = getCurrentInstance(); const reactiveData = reactive({ page: Number(root.$route.query.page || '1'), userIdModel: { key: 'userId', input: root.$route.query.userId ? Number(root.$route.query.userId) : null, state: null, }, }); const { fetchUserList, users, numberOfPage } = useList(); const linkGen = (pageNum: number): string => { return `?page=${pageNum}` + '&' + buildQuery(); }; const submitSearch = (): void => { const query = buildQuery(); window.location.href = '/list?' + query; }; const resetSearch = (): void => { window.location.href = '/list?'; }; (async(): Promise<void> => { await fetchUserList( reactiveData.page, Number(userIdModel.input) ); })(); return { ...toRefs(reactiveData as any), users, numberOfPage, linkGen, submitSearch, resetSearch, }; } }); </script>
src/composition/users/use-list.ts
Composition API
側も至ってシンプルで、API 経由でデータを取得する関数と、ユーザー情報を持つ state
を管理しています。
API のリクエストは aspida
を利用しています。詳しくはこちら 👍
import { reactive, toRefs } from '@vue/composition-api'; import { RepositoryFactory } from '@/repositories/repository-factory'; export interface User { id: number; name: string; } export interface Users { users: User[]; numberOfPage: number; } export const useList = () => { const state = reactive<Users>({ users: [], numberOfPage: 1 }); const DashboardRepository = RepositoryFactory.getDashboardRepository(); const fetchUserList = async( page: number, userId?: number, ): Promise<void> => { const response = await DashboardRepository.api.user.$get({ query: { page: page, user_id: userId, } }); state.users = []; for (const user of response.users) { state.users.push({ id: user.id, name: user.name, }); } state.numberOfPage = response.number_of_page; }; return { ...toRefs(state), fetchUserList }; };
テストについて
テストは Jest
を利用しています。
Components のテスト
コンポーネントライブラリとして BootstrapVue
を採用しているので、まずその設定を行います。
※shallowMount()
では BootstrapVue
のコンポーネントをレンダリングしないので mount()
を利用しています
ユーザーの情報をモックを利用して定義し、スナップショットテストを実行します。
import { createLocalVue, mount } from '@vue/test-utils'; import VueCompositionApi from '@vue/composition-api'; import { default as BootstrapVue, IconsPlugin } from 'bootstrap-vue'; import List from '@/components/pages/user/list.vue'; import * as useList from '@/composition/user/use-list'; const localVue = createLocalVue(); localVue.use(VueCompositionApi); localVue.use(BootstrapVue); localVue.use(IconsPlugin); const mockUseList = jest.spyOn(useList, 'useList'); describe('list.vue', () => { it('ユーザー一覧', () => { const mockValue: useList.Users = { numberOfPage: 1, users: [ { id: 1, name: 'user 1' }, { id: 2, name: 'user 2' } ], }; mockUseList.mockReturnValue({ fetchUserList: jest.fn(), numberOfPage: mockValue.numberOfPage as any, users: mockValue.users as any, }); const $route = { query: { page: 1, }, }; const wrapper = mount(List, { localVue, mocks: { $route, }, stubs: { transition: false }, }); expect(wrapper.html()).toMatchSnapshot(); }); });
Composition のテスト
aspida
のブログでも触れてましたが、aspida
のモックを作りテストを書いています。
import { createLocalVue } from '@vue/test-utils'; import VueCompositionApi from '@vue/composition-api'; import { useList, User } from '@/composition/user/use-list'; import { DashboardRepository } from '@/repositories/dashboard/repository'; const localVue = createLocalVue(); localVue.use(VueCompositionApi); type UserListType = ReturnType<typeof DashboardRepository.api.user.$get> extends Promise<infer T> ? T : never; const mockUserList = jest.spyOn(DashboardRepository.api.user, '$get'); describe('use-list.ts', () => { describe('fetchUserList', () => { it('ユーザー一覧の取得', async() => { const mockValue: UserListType = { number_of_page: 1, users: [ { id: 1, name: 'user 1' }, { id: 2, name: 'user 2' } ] }; mockUserList.mockReturnValue(mockValue as any); const { fetchUserList, users, numberOfPage } = await useList(); expect(users).toEqual([]); await fetchUserList(1); expect(numberOfPage.value).toEqual(mockValue.number_of_page); expect(users[0]).toEqual(mockValue.users[0]); }); }); });
小ネタ
検索の箇所に出てきた window.location.href
のテストも書いていきます
const submitSearch = (): void => { const query = buildQuery(); window.location.href = '/list?' + query; }; const resetSearch = (): void => { window.location.href = '/list?'; };
こちらを参考にテストを書いていきます 👍
const buildQuery = (): string => { return 'username=hoge&userId=1'; }; global.window = Object.create(window); Object.defineProperty(window, 'location', { value: { href: '' } }); expect(window.location.href).toEqual(''); submitSearch(); expect(window.location.href).toEqual('/list/?username=hoge&userId=1'); resetSearch(); expect(window.location.href).toEqual('/list');
終わりに
テストできなさそうな window.location.href
も意外とテストできたりするのは驚きました!
これからもどしどしテスト書いていきましょう!
GameWithのDeveloper向けTwitterアカウントも開設しました。
ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!