はじめに
今回の記事では、 Vue.js で単体テストを実装する方法を紹介します。 vue-test-utils を使うとテストコードを簡単に実装できるので、これを使っていきます。
Vue CLI でプロジェクトを作成した場合、テストランナーは Mocha + Chai か Jest のどちらか好きな方を選ぶことができますが、今回は Jest を使用します。どのテストランナーが良いかについてはこちらが参考になります。
参考までに、今回使用した主なライブラリ等のバージョンを記載します。
- Node.js v13.2.0
- yarn 1.17.3
- Vue CLI 4.1.1
- @vue/test-utils 1.0.0-beta.31
- @vue/cli-plugin-unit-jest 4.3.1
セットアップ
Vue CLI を利用してプロジェクトを作成する場合、対話形式で選択すれば設定が自動で完了します。今回は、 Unit test に Jest 、ほかにも、 TypeScript を使うように選択しました。
コンポーネントのテスト
実際にテストコードを書く前にコンポーネントがテストの流れを簡単に説明します。
まず初めに、 vue-test-utils から mount メソッドとテスト対象のコンポーネントをインポートします。
1 2 | import { mount } from '@vue/test-utils' import MyComponent from '@/MyComponent.vue' |
次に、コンポーネントのラッパーを作成します。
1 2 3 4 | describe('MyComponent', () => { // コンポーネントがマウントされ、ラッパーが作成されます。 const wrapper = mount(MyComponent) } |
最後にテスト内容を記述します。
1 2 3 4 5 6 | describe('MyComponent', () => { // 省略 it('renders hoge', () => { expect(wrapper.html()).toContain('<p>hoge</p>') }) } |
なお、 MyComponent の中身は以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <template> <div class="hello"> <p>{{ text }}</p> </div> </template> <script lang="ts"> import { Component, Prop, Vue } from "vue-property-decorator"; @Component export default class MyComponent extends Vue { @Prop() private text!: string; onClickBtn() { alert("hoge") } } </script> |
Shallow 描画
単体テストの場合、テスト対象のコンポーネントに焦点を当ててテストするために、子コンポーネントの振る舞いに影響されたくないと思いますが、 mount のかわりに shallowMount を使うことで子コンポーネントを(スタブによって)描画しないでテストすることができます。
1 | const wrapper = shallowMount(MyComponent) |
基本的な例
Vue CLIでプロジェクトを作成した場合、以下のようなテストコードのサンプルが作成されます。
1 2 3 4 5 6 7 8 9 10 11 12 | import { shallowMount } from "@vue/test-utils"; import HelloWorld from "@/components/HelloWorld.vue"; describe("HelloWorld.vue", () => { it("renders props.msg when passed", () => { const msg = "new message"; const wrapper = shallowMount(HelloWorld, { propsData: { msg } }); expect(wrapper.text()).toMatch(msg); }); }); |
ちなみに、テストコードは tests/unit/ 以下に xxx.spec.ts もしくは、 xxx.test.ts という形式のファイル名で作成します。
テストを実行する場合は yarn test:unit を実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ yarn test:unit yarn run v1.17.3 $ vue-cli-service test:unit PASS tests/unit/example.spec.ts HelloWorld.vue ✓ renders props.msg when passed (22ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.713s Ran all test suites. ✨ Done in 3.52s. |
プロパティを渡す
コンポーネントにプロパティを渡すには propsData オプションを使います。
1 2 3 4 5 6 7 8 9 10 | import { shallowMount } from "@vue/test-utils"; import MyComponent from "@/components/MyComponent.vue"; describe("MyComponent", () => { it("text is hoge", () => { const wrapper = shallowMount(MyComponent, { propsData: { text: 'hoge' } }); }); }); |
また、 setProps メソッドでプロパティをあとから変更することもできます。
1 2 3 4 | const wrapper = shallowMount(MyComponent, { propsData: { text: 'hoge' } wrapper.setProps({ text: 'fuga' }) }); |
ユーザーの操作をシミュレーションする
ボタンをクリックした場合など、ユーザーの操作をシミュレーションする事ができます。
この場合、まず、 find メソッドでボタン要素を探し、 trigger メソッドでクリック操作をシミュレーションします。
1 2 3 4 5 | it('button click', () => { const wrapper = shallowMount(MyComponent); const button = wrapper.find('button') button.trigger('click') }) |
イベントを検証する
マウントされた vue インスタンスに対して $emit メソッドを使うことでイベントを発行することができます。
1 2 3 4 | it('emit event', () => { const wrapper = shallowMount(MyComponent); wrapper.vm.$emit('myevent', 'hoge') }) |
発行されたイベントを検証するには emitted を使います。
1 2 3 4 5 6 | it('emit event', () => { const wrapper = shallowMount(MyComponent); wrapper.vm.$emit('myevent', 'hoge') expect(wrapper.emitted().myevent).toBeTruthy() expect(wrapper.emitted().myevent[0]).toEqual(['hoge']) }) |
グローバルプラグインとミックスイン
グローバルプラグイン(例えば VueRouter や Vuex など)やミックスインに依存したコンポーネントに対し、グローバルな Vue コンストラクタを汚染することなく独立した設定でテストしたい場合、 createLocalVue メソッドを使用することで実現することができます。
公式のサンプルコードを元に説明します。テストするコンポーネントは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <template> <div class="text-align-center"> <input type="text" @input="actionInputIfTrue" /> <button @click="actionClick()">Click</button> </div> </template> <script> import { mapActions } from 'vuex' export default { methods: { ...mapActions(['actionClick']), actionInputIfTrue: function actionInputIfTrue(event) { const inputValue = event.target.value if (inputValue === 'input') { this.$store.dispatch('actionInput', { inputValue }) } } } } </script> |
次に、テストコードは以下のとおりです。
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 { shallowMount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' import Actions from '../../../src/components/Actions' const localVue = createLocalVue() localVue.use(Vuex) describe('Actions.vue', () => { let actions let store beforeEach(() => { actions = { actionClick: jest.fn(), actionInput: jest.fn() } store = new Vuex.Store({ state: {}, actions }) }) it('dispatches "actionInput" when input event value is "input"', () => { const wrapper = shallowMount(Actions, { store, localVue }) const input = wrapper.find('input') input.element.value = 'input' input.trigger('input') expect(actions.actionInput).toHaveBeenCalled() }) it('does not dispatch "actionInput" when event value is not "input"', () => { const wrapper = shallowMount(Actions, { store, localVue }) const input = wrapper.find('input') input.element.value = 'not input' input.trigger('input') expect(actions.actionInput).not.toHaveBeenCalled() }) it('calls store action actionClick when button is clicked', () => { const wrapper = shallowMount(Actions, { store, localVue }) wrapper.find('button').trigger('click') expect(actions.actionClick).toHaveBeenCalled() }) }) |
以下のコードで拡張された Vue コンストラクタを作成し、 Vuex をインストールします。
1 2 3 | const localVue = createLocalVue() localVue.use(Vuex) |
以下のコードで Vuex.store のモックを作成します。これは beforeEach でテスト前に毎回作成されます。
1 2 3 4 5 6 7 8 9 10 | beforeEach(() => { actions = { actionClick: jest.fn(), actionInput: jest.fn() } store = new Vuex.Store({ state: {}, actions }) }) |
以下のコードで作成した Vuex.store のモックを使えるようにします。
1 | const wrapper = shallowMount(Actions, { store, localVue }) |
wrapperのプロパティとメソッド
wrapper オブジェクトのプロパティとメソッドの一部を紹介します。詳細はこちらをご覧ください。
vm | vueインスタンスで、プロパティとメソッドにアクセスできる |
element | ラッパーのルートDOM |
attributes | ラップされている要素の属性(オブジェクト) |
classes | ラップされている要素のクラス名 |
contains | 要素もしくはセレクタで指定したコンポーネントを含んでいるか |
exist | 存在するか |
find | 最初のDOMノードのwrapperもしくはVueコンポーネント |
trigger | DOMノードのイベントを発火する |
Jestのメソッド
Jest のメソッドの一部を紹介します。詳細はこちらをご覧ください。
expect | 値をテストしたい時に外のマッチャー関数と一緒に使用する |
toBe | 値もしくはオブジェクトのいくつかのプロパティを検証する |
toBeTruety | bool値でtrueであるか |
toBeFalsy | bool値でfalseであるか |
toContain | アイテムが配列内にあるか |
toMatch | 文字列が正規表現と一致するか |
さいごに
いかがでしたでしょうか。今回は基礎的なところでコンポーネントの単体テストに絞って紹介しました。この他にも、実際のプロジェクトでは axios などの非同期処理や Vuex などもテストしたい場合があると思いますので、今後機会があれば調べて見たいと思います。