特に個人で開発を行っているとテストを実施することでコードの品質を高められることはわかってはいるものの時間もかかりそうなので時間がある時にやろうとテストを後回しにしているという人も多いのではないでしょうか。

本文書ではVue CLIで作成したVue 3環境下でのVueのコンポーネントのテスト方法について説明を行っています。テストにはJavaScriptのテストフレームワークのJestとVue Test Utilsを利用しています。Jestはテストフレークワークの中でも人気が高くFaceBookによって開発されました。Vue Test UtilsはVueの公式のテストツールでVueのアプリケーションをテストするために必要な関数が含まれています。Vue.jsのビギナーの人にも理解してもらえるようにシンプルなコードを利用して動作確認を行っていきます。

Jestというテストフレームワークを利用することで複数のテストを一括で実施することができ、どのテストが成功したか失敗したかだけでなくエラーの発生場所など実施したテストに関するさまざまな情報を取得することができます。

テストの目的

正常に動作しているアプリケーションに新機能を追加後、追加したコードによってこれまで正常に動作していた機能に影響が出るかもしれないという不安が拭えないといった経験はありませんか?テストを実施することで100%不安を解消してくれるものではありませんがテストを実施することで不安を低減することができます。

テストを意識しながらテストが行えるコードを記述していくことで独立したコンポーネントとして切り出すことができます。テストができないコードは大半は複雑な機能を持っているか独立したコンポーネントとして切り出せないほど複雑に絡み合っています。テスト可能な独立したコンポーネントとして切り出すことができれば、コードを再利用できる可能性が高まる上、リファクタリング(コードの書き換え)、デバッグも楽になります。

さらにリリース後にテストの内容を確認することでテストを実施したコンポーネントがどのような動作を行うのか把握できるため設計のドキュメントとしても活用することができます。

テストは何に対してどのように行うのか

Vue.jsでは何に対してどのようにテストを実施するのかわからないという人もいるかと思います。本文書のテストではコンポーネントに対して単体テスト(Unit Test)を実施し各コンポーネントが記述したコード通りに正しく動作するのかを確認していきます。具体的にはpropsを渡されるコンポーネントであれば渡されたpropsが期待通りにブラウザ上に表示されるか。クリックイベントが設定されている場合にはクリックイベントを実行すると期待通りの処理がブラウザ上に反映されるのかをテストしていきます。

Vue新規プロジェクトの作成

Vue CLIで新規でプロジェクトを作成するのであればテスト環境を簡単に準備することができます。プロジェクトを作成する流れの中で”Manually select features(手動による機能選択)”からUnit Testを選択し、JestまたはMochaのテストフレームワークを選択するだけです。実際にVue CLIでプロジェクトの作成を行いTest環境を準備していきます。

Vue CLIについてわからない場合は下記の文書が参考になります。

Vue CLIがインストールされている環境であればVueプロジェクトの作成は任意のフォルダでvue createコマンドを実行することで開始することができます。プロジェクトには任意の名前をつけることができます。ここではvue-unit-testとしています。


 % vue create vue-unit-test 

テストフレームワークを選択するためManually select featuresを選択します。


Vue CLI v5.0.4
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 3] babel, eslint) 
  Default ([Vue 2] babel, eslint) 
  Manually select features 

必要な機能を選択してください。Unit Testは必ず選択を行ってください。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to t
oggle all, <i> to invert selection, and <enter> to proceed)
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
❯◉ Unit Testing
 ◯ E2E Testing

Vue.jsのバージョンは3.xを選択します。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Choose a version of Vue.js that you want to start the project with (Use arrow 
keys)
❯ 3.x 
  2.x 

Vue Router機能を選択している場合はヒストリモードを利用するか聞かれるので”Y”を選択しています。


Vue CLI v4.5.12
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router,
 Vuex, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x (Previe
w)
? Use history mode for router? (Requires proper server setup for index fallback 
in production) (Y/n) 

Linterの選択を行ってください。ここではESLint with error prevention onlyを選択しています。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Pick a linter / formatter config: 
❯ ESLint with error prevention only 
  ESLint + Airbnb config 
  ESLint + Standard config 
  ESLint + Prettier 

Lint on saveを選択します。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
❯◉ Lint on save
 ◯ Lint and fix on commit

Unit Testに利用するテストフレームワークの選択することができるのでJestを選択してください。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Pick a unit testing solution: (Use arrow keys)
❯ Jest 
  Mocha + Chai 

In dedicated config filesを選択します。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files 
  In package.json 

Save this as a preset for future projectsでは”N”を選択します。


Vue CLI v5.0.4
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? (y/N) 

インストールが完了するとプロジェクトフォルダ内にtestsフォルダ、jest.config.jsファイルを確認することができます。これらのファイルがテストに関連するファイル群です。またpackage.jsonファイルに”test:unit”: “vue-cli-service test:unit”,の行も追加されています。さらにpackage.jsonファイルにはテストに関連する”@vue/test-utils”: “^2.0.0-0”, “@vue/vue3-jest”: “@vue/vue3-jest”ライブラリなどが追加されていることも確認できます。


{
//略
  "devDependencies": {
    "@babel/core": "^7.12.16",
    "@babel/eslint-parser": "^7.12.16",
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-plugin-unit-jest": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
    "@vue/test-utils": "^2.0.0-0",
    "@vue/vue3-jest": "^27.0.0-alpha.1",
    "babel-jest": "^27.0.6",
    "eslint": "^7.32.0",
    "eslint-plugin-vue": "^8.0.3",
    "jest": "^27.0.5"
  }
}

既存のプロジェクトへの追加

依存のプロジェクトでテスト機能を追加したい場合は、vue add @vue/unit-jestコマンドで追加することができます。vue/unit-jestの前には@をつけて実行してください。つけない場合は、下記のエラーが表示されます。


npm ERR! 404  Not Found - GET https://registry.npmjs.com/vue-cli-plugin-unit-test - Not found
npm ERR! 404 
npm ERR! 404  'vue-cli-plugin-unit-test@*' is not in the npm registry.
npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
npm ERR! 404 

Mac環境でのインストールエラー

Unit Testing機能をインストールした時に下記のエラーが発生した場合は、xcode-select –installを実行すると問題が解消しプロジェクトの作成が正常に完了しました。


npm ERR! xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

既存のプロジェクトにvue add unit-testを実行しても同じエラーが発生する場合にもxcode-select –installを実行すると問題は解消しインストールすることができます。

手元の環境では、Unit Testingの機能をインストールしない場合は本エラーは発生せず正常にインストールは完了しました。

はじめてのテスト

最もシンプルなテスト

インストール直後にはtests/unitフォルダにexample.spec.jsファイルが保存されています。example.spec.jsファイルを利用することでインストール直後の状態でもテストを実施することができます。本文書ではデフォルトで記述されているexample.spec.jsファイルのコードは利用せずテストの理解を徐々に深めていくために最初はexample.spec.jsを使って足し算のテストを行います。

example.spec.jsファイルに下記を記述してください。テストにはtest関数を利用します。


test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

test関数の第一引数にはテストがどのような内容なのか説明を記述することができます。後に読み直してもテストの内容がわかるような説明を記述します。第2引数の関数の中にテストを記述していきます。テストでは足し算が正しく行われているか確認するためにアサーションを行う必要があります。アサーションにはexpect関数を利用して引数には値を指定します。expect関数の2+2の足し算が4になることをテストしたいのでtoBe関数に4を指定しています。toBe関数はmatchers関数の一つでexpect関数に指定した値がtoBe関数に指定した値と一致するかチェックを行います。

matchers関数によってexpectの値とマッチする条件をかえることができます。その他にもさまざまなmatchers関数が存在します。本書でもこの後にtoBe関数以外のmatchers関数を使っていきます。

test関数を使って記述していますがtest関数ではなくit関数も利用することができます。どちらを使っても同じです。


it('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

exmple.spec.jsファイルを更新したらコマンドラインでnpm run test:unitを実行することでテストが行われます。

npm run test:unitの実行メッセージを見るとtest関数の第一引数のテスト内容の説明two plus two is fourをメッセージの中で確認することができます。PASSと表示されればこのテストが成功したことを意味します。


 % npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/example.spec.js
  ✓ two plus two is four (2ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.44s
Ran all test suites.

テストにFAIL(失敗した場合)はどのような表示になるか確認しておきましょう。先ほどexpect関数の値を2+2にしましたが1+2に変更します。toBe関数でexpect関数に指定した値が4になることを期待していますが1+2がexpect関数に指定されるためtoBeで設定した値と一致せずテストは失敗します。


test('two plus two is four', () => {
  expect(1 + 2).toBe(4);
});

変更後にnpm run test:unitを実行すると実行メッセージにはPASSではなくFAIL(失敗)の文字列を確認することができます。4になると期待されている値(Expected: 4)に3が入っていた(Received: 3)ということも実行メッセージからわかります。テストのPASS, FAILだけではなくテストのどの部分にエラーがあったかのヒントも教えてくれます。


% npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 FAIL  tests/unit/example.spec.js
  ✕ two plus two is four (4ms)

  ● two plus two is four

    expect(received).toBe(expected) // Object.is equality

    Expected: 4
    Received: 3

      1 | test('two plus two is four', () => {
    > 2 |   expect(1 + 2).toBe(4);
        |                 ^
      3 | });
      4 |

      at Object.<anonymous> (tests/unit/example.spec.js:2:17)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.377s
Ran all test suites.

コンポーネントを使ったテスト

先ほどはただの足し算でしたが次は本来のテストの目的であるVue.jsのコンポーネントを利用してテストを実施してみましょう。引き続きexample.spec.jsファイルを利用します。ファイル名を変更することも可能です。もし変更する場合のファイル名は*.spec.jsとしてください。(*:任意の名前)

example.spec.jsファイルの中身を削除して、vue/test-utilsからmount関数をimportします。vue/test-utilsはVue Test Utilsライブラリを表しています。Vue Test UtilsライブラリはVueの公式のUnit TestのツールでVue.jsをテストを行うために必要となる関数が含まれています。このライブラリを利用することでVue.jsのコンポーネントのテストを効率的に行うことができます。


import { mount } from '@vue/test-utils';

最初はシングルファイルコンポーネントではなくコンポーネントオブジェクトを作成してテストを実行します。下記のコードをexample.spec.jsに記述してください。


import { mount } from '@vue/test-utils';

const App = {
  template:`
  <div>Hello World</div>
  `
}

test("test App Component",function(){
  const wrapper = mount(App);
  console.log(wrapper.text())
})

importしたmount関数の中にAppコンポーネントを指定することでWrapperオブジェクトを作成することができます。Wrapperオブジェクトはマウントされたコンポーネント情報が含まれるたけでなくテストに利用できるメソッドも持っています。Wapperオブジェクトはテストのコードの中でconsole.log(Wrapper)することでオブジェクトの中身を確認することができます。

WrapperオブジェクトについてはVue Test Utilsのドキュメントでメソッドなど詳細を確認することができます。このドキュメントも利用しながらテストを進めていきます。

Vue Test Utilsのドキュメント
Vue Test Utilsのドキュメント

Vue Test UtilsのドキュメントからWrapperのtextメソッドを確認するとtextメソッドを使って要素のテキストコンテンツが取得できることがわかります。example.spec.jsファイルのテストコードではconsole.logでwrapper.text()を使ってAppコンポーネントの要素の中身を表示させています。

expect関数による値のチェックなどは行っていませんがこの状態でもテストを実施することが可能です。実行するとテストはPASS(成功)し、実行メッセージの中で要素の中身であるHello Worldを確認することができます。


 % npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/example.spec.js
  ✓ test App Component (34ms)

  console.log tests/unit/example.spec.js:11
    Hello World

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.041s
Ran all test suites.

Helloコンポーネントの中に複数の要素が存在する場合には下記のように表示されます。


const App = {
  template: `
  <div>
  Hello World
  <p>test</p>
  <ul>
  <li>TESTA</li>
  <li>TESTB</li>
  <li>TESTC</li>
  </ul>
  </div>
  `,
};

テストを実行するとテキストの中身だけ表示されます。


  console.log
    Hello World testTESTATESTBTESTC
要素のコンテンツではなく要素のHTMLを取得したい場合はwrapper.text()ではなくwrapper.htm()を使います。htmlメソッドの場合は<div>Hello World</div>が取得できます。

textメソッドによってコンポーネントのコンテンツが取得できることがわかったのでexpect関数とtoBe関数を使ってテストを実行します。expect関数の引数にコンポーネントの要素のコンテンツを取得するwrapper.text()を指定してHello Worldと一致するかチェックしています。


import { mount } from '@vue/test-utils';

const App = {
  template:`
  <div>Hello World</div>
  `
}

test("test App Component",function(){
  const wrapper = mount(App);
  expect(wrapper.text()).toBe('Hello World')
})

実行するとテストはPASS(成功)します。シンプルな例ですがVueのコンポーネントをテストする方法を確認することができました。


 % npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/example.spec.js
  ✓ test App Component (16ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.849s, estimated 2s
Ran all test suites.

toBe関数の引数の文字列を変更することでテストがFAIL(失敗)することも確認しておいてください。

シングルファイルコンポーネント(SFC)でのテスト

次はコンポーネントオブジェクトではなくシングルファイルコンポーネントを利用してテストを実施してみましょう。src¥componentsフォルダにApp.vueファイルを作成して下記を記述してください。srcフォルダ直下にあるApp.vueではないので注意してください。


<template>
  <div>Hello World</div>
</template>

example.spec.jsファイルでは作成したApp.vueをimportします。Appコンポーネントをimportする以外は先ほど実行したテスト内容と同じです。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

test("test App Component",function(){
  const wrapper = mount(App);
  expect(wrapper.text()).toBe('Hello World')
})

テストを実行するとシングルページコンポーネントを使ってテストが実施でき、テストにPASS(成功)できていることが確認できます。


 % npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/example.spec.js
  ✓ test App Component (13ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.223s, esti

ここまででシングルファイルコンポーネントでのテスト方法まで理解することができました。初めてVue.jsを含めフロントエンドのフレームワークをJestを利用してテストを実施した人にとってコンポーネントのUnit Test(単体テスト)をどのようにテストするのか想像もつかなかったと思います。今回実行してみるとシンプルなコンポーネントであればそれほど難しいないのではないかと思われたのではないでしょうか。

今後はシングルファイルコンポーネントを利用してテストを実施していきます。

Propsを利用したテスト

コンポーネントはtemplateタグのHTMLを表示させるだけではなく通常は、props、dataプロパティ、イベント、slot、メソッド、computedプロパティなどさまざまな要素の組み合わせて表示させる内容を変更することができます。

Appコンポーネントにpropsが渡される場合、テストではどのようにコンポーネントに対してpropsを渡し、その渡したpropsをテストすることができるのか確認します。

Appコンポーネントでmsgという名前でpropsを受け取れるように設定します。msgの型はStringの文字列に設定しています。


<template>
  <div>Hello {{ msg }}</div>
</template>
<script>
export default {
  props:{
    msg:{
      type:String
    }
  }
}
</script>

単体テストはコンポーネント毎にテストを行うので他のコンポーネントからpropsを渡すのではなくテスト内でpropsを渡す必要があります。

mount関数は、Appコンポーネントをマウントする際に第2引数にコンポーネントに渡すpropsを設定することができます。

Vue Test Utilsのドキュメントのmountの説明を確認すると第2引数にはprops以外のものもマウントオプションとして渡せることがわかります。

下記ではmount関数の第2引数のマウントオプションを利用してpropsのmsgにWorldの文字列を渡しています。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

test("test App Component",function(){
  const wrapper = mount(App,{
    props:{
      msg: "World"
    }
  });
  expect(wrapper.text()).toBe('Hello World')
})

テストを実行するとpropsでWorldの文字列が渡されるのでAppコンポーネントのコンテンツはHello Worldになります。そのためテストにPASS(成功)することができます。


% npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/example.spec.js
  ✓ test App Component (13ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.957s
Ran all test suites.

渡されたpropsの値は、Wrapperオブジェクトのpropsメソッドで確認することができます。


console.log(wrapper.props())
// { msg: 'World' }

コンポーネントをテストする際のpropsの扱い方について理解することができました。

ここまではmatchers関数としてtoBe関数のみ利用していましたが文字列の一部の一致をチェックしたい場合はtoMatch関数、toContain関数を利用することができます。後ほど別のテストで利用します。

matchers関数によってexpectの値とマッチする条件をかえることができます。

expect(wrapper.text()).toContain('World')
expect(wrapper.text()).toMatch('World')

matchers関数についてはVue Test UtilsのメソッドではなくJestのメソッドなのでどのようなメソッドがあるのか確認したい場合はJestのドキュメントを確認する必要があります。

Jestのドキュメンテーション
Jestのドキュメント

vueインスタンスへのアクセス

テストコードでのvueインスタンスへのアクセスはwapper.vmで行うことができます。

データプロパティadminを持つAppコンポーネントで確認します。


<template>
  <div>Hello</div>
</template>
<script>
export default {
  data() {
    return {
      admin: true,
    };
  },
};
</script>

const wrapper = mount(App);
console.log(wrapper.vm)
//
  console.log
    { admin: [Getter/Setter], hasOwnProperty: [Function (anonymous)] }

データプロパティもVueインスタンスから取得することができます。データプロパティadminが設定されている場合は下記のようにアクセスを行います。


const wrapper = mount(App);
console.log(wrapper.vm.admin)
//
  console.log
    true

computedプロパティのテスト

computedプロパティが期待通りに動作しているかテストを実施することができます。設定したデータプロパティnameの値がcomputedプロパティによって大文字になっているかのテストを実施します。


<template>
<div>{{ upperCaseName }}</div>
</template>
<script>
export default {
  data(){
    return {
      name: 'John'
    }
  },
  computed:{
    upperCaseName(){
      return this.name.toUpperCase()
    }
  }
}
</script>

wapperオブジェクトのtextメソッドでAppコンポーネントのコンテンツを取得し、データプロパティnameのjohnがcomputedプロパティにより大文字のJOHNになっているかtoBE関数を使ってチェックしています。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("computed property upper case",() => {
    const wrapper = mount(App);
    expect(wrapper.text()).toBe('JOHN')
  })
})

テストを実行するとComputedプロパティにより期待通りにjohnが大文字のJOHNになりテストはPASS(成功)します。テストでは表示されている内容を確認しているのでテスト内ではComputedプロパティを指定するような処理はありません。

describe関数について

テストコードではtest関数またはit関数の外側にdescribe関数を記述することができます。describe関数の中には複数のtest関数、it関数を記述することができます。これまでは1つのtest関数のみ実行していたためdescribe関数は必要ではありませんが複数のtest関数を実行する場合はdescribe関数で個別のtest関数をグループ化することができます。

describe関数の第一引数には何に対するテストを行うかの説明を記述することができるためここではコンポーネントの名前を設定しています。下記ではdecribe関数の動作確認を行うためdescribe関数の中にtest関数とit関数の2つの関数を記述しています。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("test App Component",() => {
    const wrapper = mount(App,{
      props:{
        msg: "World"
      }
    });
    expect(wrapper.text()).toBe('Hello World')
  })
  test("test App Component again",function(){
    const wrapper = mount(App,{
      props:{
        msg: "World"
      }
    });
    expect(wrapper.text()).toMatch('Hello World')
  })
})

テストを行うAppコンポーネントは下記です。


<template>
  <div>Hello {{ msg }}</div>
</template>
<script>
export default {
  props:{
    msg:{
      type:String
    }
  }
}
</script>

テストを実行すると2つのテストが実行されどちらのテストもPASSしていることがわかります。describe関数はTes Suitesとも呼ばれ実行メッセージに表示されているようにテストの単位ともなります。今回は2つのテストが成功したのでTest Suites 1つが成功、Testが2つ成功したことになります。


% npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/example.spec.js
  App
    ✓ test App Component (12ms)
    ✓ test App Component again (2ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.05s
Ran all test suites.

どちらかのテストがFAIL(失敗)した場合は下記のように表示されます。2つのうち1つのテストが失敗したのでTest Suitesは失敗したことがわかります。


 % npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 FAIL  tests/unit/example.spec.js
  App
    ✓ test App Component (12ms)
    ✕ test App Component again (3ms)

  ● App > test App Component again

    expect(received).toMatch(expected)

    Expected substring: "Hell World"
    Received string:    "Hello World"

      17 |       }
      18 |     });
    > 19 |     expect(wrapper.text()).toMatch('Hell World')
         |                            ^
      20 |   })
      21 | })
      22 |

      at Object.<anonymous> (tests/unit/example.spec.js:19:28)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.961s, estimated 2s

1つのテストだけ実行したい

describeで設定したtest関数(it関数)の中で1つだけ実行したい場合はtestの後にonlyをつけることでその他のtest関数をスキップしてonlyを指定したtest関数のみ実行することができます。


describe('App', () => {
  it.only("test App Component",() => {
    //このテストのみ実行されます。
  })
  test("test App Component again",function(){
    //このテストはスキップされます。
  })
})

条件(v-if, v-show)による表示テスト

データプロパティのadminの値によって要素の表示・非表示が変わるAppコンポーネントを作成してテストを実行します。v-ifの条件によるテストを通してWrapperオブジェクトのget、find、 exists、 isVisibleメソッド、setDataメソッドによるデータプロパティの設定方法の使い方も確認していきます。

getメソッド, findメソッド

App.vueファイルに以下のコードを記述します。データプロパティadminを設定し、v-ifディレクトリにadminの値を設定することでadminがtrueの場合はAdminのリンクが表示され、falseの場合はAdminのリンクが表示されないといったシンプルなコードです。


<template>
    <nav>
      <a id="profile" href="/profile">My Profile</a>
      <a v-if="admin" id="admin" href="/admin">Admin</a>
    </nav>
</template>
<script>
export default {
  data(){
    return {
      admin: false
    }
  }
}
</script>

これまではmount関数を利用してWapperオブジェクトを取得し、textメソッドによってコンポーネントのコンテンツの情報を取得してきました。Appコンポーネントが複数の要素で構成されている場合はWrpperオブジェクトのgetメソッドを利用して特定の要素のDOMWrapperオブジェクトを取得することができます。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("test App Component",function(){
    const wrapper = mount(App);
    const profile = wrapper.get('#profile'); //DOMWrapper
    console.log(profile.text());
  })
})

DOMWrapperもWrapperオブジェクトと同様にtextメソッドを持っているためid=”profile”を持つ要素のコンテンツ”My Profile”を取得することができます。

getメソッドだけではなくfindメソッドも同じように引数に指定した要素のコンテンツを取得することができます。


const profile = wrapper.find('#profile');
console.log(profile.text())
//My Profile

どちらも同じDOMWrapperを戻しますがgetメソッドの場合は要素が存在しない場合にエラーとなります。getメソッドとfindメソッドの違いを確認していきます。

example.spec.jsではデータプロパティのadminの値がfalseのためv-ifディレクティブによりid=”admin”を持つ要素は表示されていません。まずは要素が表示されていない状態でテストを実行します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("test App Component",function(){
    const wrapper = mount(App);
    const admin = wrapper.get('#admin'); //DOMWrapper
    console.log(admin.text());
  })
})

テストを実行するとgetメソッドの実行時にエラーが発生していることがメッセージから確認することができテストはFAIL(失敗)します。


● App › test App Component

Unable to get #admin within: <nav><a id="profile" href="/profile">My Profile</a><!--v-if--</nav>

    5 |   it("test App Component",function(){
    6 |     const wrapper = mount(App);
>  7 |     const admin = wrapper.get('#admin');
      |                             ^
    8 |     console.log(admin.text())

getメソッドからfindメソッドに変更して再度テストを実行します。


const admin = wrapper.find('#admin');
console.log(admin.text())

findメソッドの場合はfindメソッド実行時の時点ではエラーは発生しません。しかしprofileのtextメソッドを実行した時にDOMWrapperには何も入っていないためエラーになります。


● App › test App Component

  Cannot call text on an empty DOMWrapper.

      6 |     const wrapper = mount(App);
      7 |     const admin = wrapper.find('#admin');
  >  8 |     console.log(admin.text())
        |                         ^
      9 |   })
    10 | })

getメソッドとfindメソッドでは指定した要素が存在する場合はどちらもDOMWrapperを戻しますが存在しない場合に動作の違いがあることを理解しておく必要があります。

findメソッドについてはexistsメソッドを組み合わせることでfindで指定した要素が存在するかどうかのテストを行うことができます。existsメソッドでは戻り値がtrueかfalseなのでtoBe関数での値はtrueかfalseを指定します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("test App Component",function(){
    const wrapper = mount(App);
    const admin = wrapper.find('#admin');
    expect(admin.exists()).toBe(false)
  })
})

id=”admin”の要素は非表示なのでprofile.exists()の値はfalseになるためテストを実行するとPASS(成功)します。toBe関数の値をfalseからtrueに変更するとテストはFAIL(失敗)します。

dataプロパティの設定

dataプロパティのadminの値をテストの中でfalseからtrueに設定することで非表示から表示へと変更を行いテストを実行します。dataプロパティのadminの初期値はfalseですが、propsと同様にmount関数のマウントオプションを利用してdataプロパティの値を上書きすることができます。

マウントオプションではprops、dataの値を一緒に設定することも可能です。

import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("test App Component",function(){
    const wrapper = mount(App,{
      data(){
        return {
          admin: true,
        }
      }
    });
    const admin = wrapper.find('#admin');
    expect(admin.exists()).toBe(false)
  })
})

テストを実施するとadminがtrueになっているためid=”admin”を持つ要素が表示になっているためadmin.existsの結果はtrueになりtoBe関数の値falseと一致しないためテストはFAIL(失敗)します。

今度はtoBe(false)からtoBe(true)に変更するとテストはPASS(成功)します。

mount関数のオプションからだけではなくWrapperオブジェクトのsetDataメソッドでもデータプロパティの値を上書きすることができます。

propsもデータプロパティと同様にマウントオプションだけではなくsetPropsメソッドでテスト中にpropsを設定することができます。

describe('App', () => {
  it("test App Component",function(){
    const wrapper = mount(App);
    wrapper.setData({
      admin:true
    })
    const admin = wrapper.find('#admin');
    expect(admin.exists()).toBe(true)
  })
})

setDataを設定した後テストを実行するとsetDataでadminの値を上書きしたにも関わらずテストはFAIL(失敗)します。setDataでadminの値を上書き処理を実行してもまだDOMには上書きした値が反映されていないためexpect関数を実行した時にはまだ要素は非表示のままです。DOMが更新されてからexpectでのアサーションが行えるように非同期async/await関数を利用します。


describe('App', () => {
  it("test App Component",async () => {
    const wrapper = mount(App);
    await wrapper.setData({
      admin:true
    })
    const admin = wrapper.find('#admin');
    expect(admin.exists()).toBe(true)
  })
})

async/await関数を設定した後に再度テストを実行するとテストはPASS(成功)します。setDataを利用する場合はasync/await関数を忘れずに設定する必要があります。

データプロパティの設定について2つの方法を確認しましたが、mountオプションの場合はコンポーネントをマウントする際にdataプロパティへの設定が行われます。setDataの場合はコンポーネントのマウント後のテスト中に値が設定されます。どちらもdataプロパティの値を上書きすることができますが違いを理解した上で使い分ける必要があります。

v-showの場合の動作

v-showの動作確認を行う前にv-showとv-ifにはどのような違いがあるか確認しておきましょう。v-ifではv-elseを使うことでv-ifの条件に一致しない場合はv-elseを設定した要素を表示されることができるという違い以外に表示に関する違いがあります。

v-showの場合は表示・非表示をstyle属性のdisplayの値によって制御します。表示の場合displayの値はblock, 非表示の場合は値がnoneになります。一方v-ifの場合は表示の場合のみ要素が追加され、非表示の場合は要素自体が存在しません。

そのためv-showの場合はgetメソッドを使ってもgetメソッドで要素を取得することができるためエラーが発生するということはありません。(表示・非表示のどちらでも要素自体は存在するため)

App.vueファイルのv-ifディレクティブをv-showディレクティブに変更します。


<template>
    <nav>
      <a id="profile" href="/profile">My Profile</a>
      <a v-show="admin" id="admin" href="/admin">Admin</a>
    </nav>
</template>
<script>
export default {
  data(){
    return {
      admin: false
    }
  }
}
</script>

adminをfalseにしたままgetメソッドを利用します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("test App Component",() => {
    const wrapper = mount(App);
    const admin = wrapper.get('#admin');
    expect(admin.exists()).toBe(true)
  })
})

テストを実行するとadminの値がfalseでもtrueでもPASS(成功)します。style属性のdisplayプロパティの値が変わるだけでid=”admin”を持つ要素が存在しているためです。

v-showの場合にテストが実施できるようにisVisibleメソッドを利用することができます。adminがfalseの状態でテストを実行するとテストはFAIL(失敗)します。isVisibleメソッドでは要素の存在ではなくブラウザ上に表示されているかどうかを確認することができるためです。


describe('App', () => {
  it("test App Component",() => {
    const wrapper = mount(App);
    const admin = wrapper.get('#admin');
    expect(admin.isVisible()).toBe(true)
  })
})

v-ifとv-showはブラウザ上では表示・非表示を制御するという機能は同じですが、それを実現するための方法が異なるということを理解した上でテストの方法も変える必要があります。

クリックイベントのテスト

Vue.jsではユーザとのインタラクションを実現するために各所でイベントを設定していきます。イベントの中でも頻繁に利用するクリックイベントが設定されている場合のテストの方法を確認していきます。

App.vueファイルを下記のように更新します。データプロパティcountを設定し初期値を0とします。button要素にclickイベントを追加し、ボタンをクリックするとincrementメソッドを実行し、countの値を1増やします。


<template>
    <p>Count is: {{ count }}</p>
    <button @click="increment">+</button>
</template>
<script>
export default {
  data(){
    return {
      count: 0
    }
  },
  methods:{
    increment(){
      this.count += 1;
    }
  }
}
</script>

triggerメソッド

初めてクリックイベントに対してテストを実施する人であればどのようにテストコードの中でボタンをクリックするのかわからないのでまずはボタンをクリックする方法を確認します。

ボタンをテストの中でクリックするためにはbutton要素をgetメソッド(またはfindメソッド)で取得し、DOMwrapperのtriggerイベントを実行します。


const wrapper = mount(App);
wrapper.get('button').trigger('click')

クリック後はcountの数が1増えているのでtoContain関数を利用してwapperのコンテンツの一部にCount is: 1が含まれていればテストはPASS(成功)します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("click button count up", () => {
    const wrapper = mount(App);
    wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
})

しかし、実際にテストを実行するとFAIL(失敗)します。


 FAIL  tests/unit/example.spec.js
  App
● App › click button count up

  expect(received).toContain(expected) // indexOf

  Expected substring: "Count is: 1"
  Received string:    "Count is: 0+"

データプロパティをsetDataメソッドで実行した時にFAILした時と原因は同じです。buttonをテスト中にクリックしても即座にDOMが更新されていないためです。setDataと同様に非同期関数のasync/await関数を利用します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("click button count up", async () => {
    const wrapper = mount(App);
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
})

async/awaitを追加後に再度テストを実行するとテストはPASS(成功)します。

triggerメソッドについてはclickイベントだけではなくsubmit、keyupイベントでも利用することができます。submitイベントについては後ほどフォームでのテストで利用します。

nextTickを使ったDOM更新

async/awaitではなくnextTickを利用してもDOMの更新を行うことができます。先ほどはtriggerメソッドの行にawaitを設定していましたが、triggerメソッドの後にnextTickを実行することでDOMの更新が行われテストもPASS(成功)します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";
import { nextTick } from "vue"

describe('App', () => {
  it("click button count up", async () => {
    const wrapper = mount(App);
    wrapper.get('button').trigger('click')
    await nextTick()
    expect(wrapper.text()).toContain('Count is: 1')
  })
})

ドキュメントではどちらの方法も記載されていますがnextTickを利用しないほうが簡単でクリーンな方法と言っています。

nextTickについてはvueからimportしていましたが、wrapperオブジェクトのvmプロパティからVueインスタンスにアクセスすることができるためwapper.vmを利用してnextTickを実行することも可能です。その場合はnextTickをvueからimportする必要はありません。


await wrapper.vm.$nextTick()

beforeEachの使い方

describe関数の中には複数のテストを記述することを説明しましたが複数の中で同じ処理を実行している場合冗長となるため1つにまとめたいということがあるかもしれません。そのような時にbeforeEachを利用することができます。例えばCountアップのテストで2つのテストを実施するとします。(通常は異なるテスト内容を記述しますがbeforeEachの説明に注目するためテスト内容は同じです)


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("click button count up", async () => {
    const wrapper = mount(App)
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
  it("click button count up", async () => {
    const wrapper = mount(App)
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
})

どちらのテストでも同じconst wrapper = mount(App)を実行しているのでこの関数を一つにまとめたいとします。test関数の外側でmount(App)を実行することができ、テストも実行することが可能です。


describe('App', () => {

  const wrapper = mount(App);

  it("click button count up", async () => {
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
  it("click button count up", async () => {
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
})

しかし、テストを実行すると2つ目のテストに失敗します。2つのテストの中でclickイベントを2回実行することになるためcountが2つ目のテストで2になるためです。テスト毎処理を初期化するためにbeforeEachを利用することができます。beforeEachを利用することでテスト毎にmount関数が実行されるためテストは成功します。


describe('App', () => {

  let wrapper;

  beforeEach(() => {
    wrapper = mount(App)
  })

  it("click button count up", async () => {
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
  it("click button count up", async () => {
    await wrapper.get('button').trigger('click')
    expect(wrapper.text()).toContain('Count is: 1')
  })
})
wrapperを定義する際はconstではなくletを利用します。constを利用すると値が上書きできないのでエラーになります。

テスト後に何か共通の処理を実施したい場合はafterEach(()=>{})を利用することができます。すべてのテストの前に一度だけ実行したい共通の処理がある場合はbeforeAll(()=>{})を利用することができます。

emitイベントのテスト

先ほどpropsのテスト方法を確認しましたたが、propsとは逆にVue.jsでは子コンポーネントから親コンポーネントにデータを渡す際はemitイベントを利用します。emitイベントをどのような方法でテストを行うかを確認します。

App.vueファイルを下記のように更新します。


<template>
    <button @click="increment">+</button>
</template>
<script>
export default {
  data(){
    return {
      count: 0
    }
  },
  methods:{
    increment(){
      this.count += 1;
      this.$emit('incrementCount',this.count)
    }
  }
}
</script>

ボタンをクリックするとclickイベントによりincrementメソッドが実行され、emitメソッドにはイベント名をincrementCountとしてpayloadに設定したthis.countの値が親コンポーネントに渡されます。

テストではemitによって発火されたイベントincrementCountを取得する必要があります。emitイベントはwapperオブジェクトのemittedメソッドにより取得することができます。emittedの引数にはAppコンポーネントのイベント名であるincrementCountを指定します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("click button count up",() => {
    const wrapper = mount(App);
    wrapper.get('button').trigger('click')
    console.log(wrapper.emitted('incrementCount'))
  })
})

テストを実行するconsole.logによって取得したincrementCountイベントのpayloadを確認することができます。


 PASS  tests/unit/example.spec.js
  App
    ✓ click button count up (35ms)

  console.log tests/unit/example.spec.js:9
    [ [ 1 ] ]

配列で保存されているのでthis.countの値はwrapper.emitted(‘incrementCount’)[0][0]でアクセスすることができます。

toBe関数を使ってemitイベントから取得した値をチェックすることができます。テストを実行するとPASS(成功)します。


describe('App', () => {
  it("click button count up", async () => {
    const wrapper = mount(App);
    wrapper.get('button').trigger('click')
    expect(wrapper.emitted('incrementCount')[0][0]).toBe(1)
  })
})

Appコンポーネント側で複数の値を渡すことも可能です。


this.$emit('incrementCount',this.count,'test')

その場合はテスト側では下記のような配列で取得することができます。


  console.log tests/unit/example.spec.js:8
    [ [ 1, 'test' ] ]

複数回clickイベントを実行した場合のデータも確認しておきます。


describe('App', () => {
  it("click button count up", async () => {
    const wrapper = mount(App);
    wrapper.get('button').trigger('click')
    wrapper.get('button').trigger('click')
    console.log(wrapper.emitted('incrementCount'))
  })
})

配列に追加される形で2回目のemitイベントのpayloadが保存されます。


  console.log tests/unit/example.spec.js:9
    [ [ 1, 'test' ], [ 2, 'test' ] ]

フォームのテスト

入力フォームのテスト方法を確認するためApp.vueファイルを下記のように更新します。input要素が一つだけのシンプルなフォームでSubmitボタンをクリックするとformタグに設定したsubmitイベントによりsubmitFormメソッドが実行されます。submitFormメソッドではemitイベントによってsubmittedというイベント名でinput要素で入力した値が保存されているデータプロパティのnameを親コンポーネントに渡します。


<template>
  <form @submit.prevent="submitForm">
    <input type="text" v-model="name" />
    <button type="submit">Submit</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      name: ''
    }
  },
  methods: {
    submitForm() {
      this.$emit('submitted', { name: this.name })
    }
  }
}
</script>

submitイベントについてはclickイベントと同様にtriggerメソッドを利用することができます。今回もemitイベントを使っていますが前回とは異なりpayloadにオブジェクトを指定しています。

通常はinput要素にはブラウザ上でユーザが入力を行います。テストコードの中でどのようにinput要素へ値を設定するのかわからないと思うのでまずはその方法を確認します。

input要素への値を設定はsetValueメソッドを使って行います。ここではinput要素が1つしかないためgetメソッドでinputしか指定しませんが、複数のinputがある場合はinput要素にidなど識別できる属性を設定する必要があります。


const input = wrapper.get('input')
input.setValue('John Doe')

input要素にsetValueメソッドでJohn Doeを設定した後triggerメソッドでsubmitイベントを実行します。


import { mount } from '@vue/test-utils';
import App from "@/components/App.vue";

describe('App', () => {
  it("receive name from App form",() => {
    const wrapper = mount(App);
    const input = wrapper.get('input')
    input.setValue('John Doe')
    wrapper.trigger('submit')
    console.log(wrapper.emitted('submitted'))
  })
})

テストを実行するとconsole.logによりsubmittedイベントで受け取ったpayloadが表示されます。配列の中にオブジェクトが入っていることがわかります。


 PASS  tests/unit/example.spec.js
  App
    ✓ receive name from App form (44ms)

  console.log tests/unit/example.spec.js:10
    [ [ { name: 'John Doe' } ] ]

アサーションにはmatcher関数のtoEqual関数を使います。toEqual関数で配列のデータが一致するかチェックを行うことができます。


expect(wrapper.emitted('submitted')[0][0]).toEqual({
  name:"John Doe"
})

テストを実行するとPASS(成功)することが確認できます。input要素のみのシンプルなフォームですがフォームのテスト方法を確認することができました。

ここまででVue.jsのコンポーネントのテストに関する基礎的な使用方法を理解することができました。さらにテストについて知りたい場合下記の記事がお勧めです。