Vue Test Utils のチュートリアルで手を動かす

はじめに

jestのチュートリアルが終わったので、次のステップとしてVueのユニットテストを書けるようになりたいと思います。

v1.test-utils.vuejs.org

インストール

テストランナを選ぶ

  • テストを実行するプログラムであるテストランナを選ぶ
  • vue-test-utilsはどれでも動作する。テストランナにとらわれない
  • jest
    • 最も充実したテストランナ
    • デフォルトでJSDOMを設定する(何もしなくてもDOMが使えるということかな)
    • SFCコンポーネントをインポートできるようにするにはプリプロセッサが必要
      • vue-jestプリプロセッサがあるが、vue-loaderと100%同じというわけではない
        • SFC機能のうち、処理できない書き方とかあるということかな
  • mocha-webpack
    • webpack+Mochaのラッパ
    • vue-loaderを使って、完全なSFCサポートが得られる
    • 多くの設定が必要

ブラウザ環境

  • vue-test-utilsはブラウザ環境に依存する
    • どういうこと?
      • 実際のブラウザで実行することもできる(らしい)
      • でもおすすめはJSDOMを使用して仮想ブラウザ環境でNode.jsでテストすること
      • jestならJSDOMは自動で設定される

単一ファイルコンポーネントをテストする

  • SFCをテストするには、事前コンパイルが必要
  • vue-jestは基本的なSFC機能をサポート
    • スタイルブロック、カスタムブロックは扱えない
  • ガイドがある

v1.test-utils.vuejs.org

Jestを使用したSFCのテスト

Jestのセットアップ

# まずはvueのプロジェクトを作る
$ vue create vtu_sample
$ cd vtu_sample
$ npm run serve # で動くことを確認

# ライブラリいれる
$ npm install --save-dev jest @vue/test-utils
# あー、vueのバージョンを2.6xで入れたから、エラーになるな。。
# @vue/test-utilsがv2になっているので、v1を入れる必要がありそう。
$ npm info @vue/test-utils versions
# '1.3.0'が1系の最新
$ npm i -D jest @vue/test-utils@1.3.0
# インストール成功
//package.jsonのscript以下追記
    "test" : "jest"

jestに*.vueファイルの処理方法を教えるために、vue-jsetプリプロセッサを入れる。

$ npm i -D vue-jest

package.jsonにjestの設定を記載。(jest.config.jsonじゃなくてpacakge.jsonに追記する書き方か)

  "jest": {
    "moduleFileExtensions" : [
      "js",
      "json",
      "vue"
    ],
    "transform": {
      ".*\\.(vue)$": "vue-jest"
    }
    // @を/srcのエイリアスにしたい場合の設定
   // jsconfig.jsonのpathsで設定しているやつ。
   // webpackなら、webpack.config.jsのresolve.aliasに`"@" : path.resolve(__dirname,"src")`と設置するぽい
  // TSなら,tsconfig.jsonのcompilerOptions.pathsに` { "@/*" : ["src/*"]}`
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    }
  }

jestのためのBabelの設定

  • うーん、テストでES Modules構文とstage-x機能を利用するにはbabel-jestのインストールが必要と
  • Nodeの最新バージョンでもESMは対応しているけど、CJSとの併用で難点ありそうだから、これはいれないとESM使えないのかな
  • jestテストはNodeで直接実行されるので、テスト用に babel-jestを有効にする必要がある、と記載があるな

いったん、 入れないでおこうかな動くかもしれないし。

簡単に、jsのテストを書いてみたら、import文動いたな。 人知れずbabelが動いているわけでもなさそうだし。なくてもいいんかな。

いや、動かなくなったなぜだ。。。とりあえず設定したらええんかな。

$ npm i -D babel-jest

package.jsonのjest.transformに以下追記

 "^.+\\.js$": "<rootDir>/node_modules/babel-jest"

単純なテストは成功する用になったが、vueコンポーネントのテストではjestのオプションが不正っぽいエラーがでる。

ググって、babel-coreのバージョンを上げる方法がある(issue)、ということで、以下でバージョン上げた。

$ npm i -D babel-core@^7.0.0-bridge.0

テストファイルの配置

  • デフォルトは.spec.jsまたは.test.js
  • package.jsonで定義可能
  • 推奨
    • テスト対象のコードのすぐとなりに__test__ディレクトリを作成
  • jsetがスナップショットテストをするとき
    • テストファイルのとなりに__snapshots__ディレクトリを作成する

これ、大事やん。__test__をテスト対象コードの同階層に配置するのが推奨なのか。

カバレッジ

  • jestの設定にcollectCaverageオプションでカバレッジ取れる
{
  "jest": {
    // ...
    "collectCoverage": true,
    "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"]
  }
}
  • デフォルトのカバレッジレポーターのカバレッジレポートは有効になる
    • Default: ["clover", "json", "lcov", "text"]
  • coverageReportersでカスタマイズも可能
    • ["html", "text-summary"]

Configuring Jest · Jest

Specの例

テスト失敗した。

 [vue-test-utils]: window is undefined, vue-test-utils needs to be run in a browser environment. 
    You can run the tests in node using jsdom 
    See https://vue-test-utils.vuejs.org/guides/#browser-environment for more details.

おー、jestのv28からjsdomが標準でインストールされなくなったのか。

Jest v28に上げるためにやったこと

$ npm i -D jest-environment-jsdom

package.jsonのjestブロックに以下を追記。

 testEnvironment: "jest-environment-jsdom"

これで、テストは実行されるようになった。

だが、以下warningが出た。

  console.error
      [vue-test-utils]: isVueInstance is deprecated and will be removed in the next major version.

       5 |   it("is a Vue instance", () => {
       6 |     const wrapper = mount(HelloWorld);
    >  7 |     expect(wrapper.isVueInstance()).toBeTruthy();
         |                    ^
       8 |   });
       9 | });
      10 |

このwarning(error?)は、isVueInstance()がdeprecatedということを伝えるメッセージだった。将来的なバージョンでは削除されるらしい。

スナップショットの更新

jest --updateSnapshot

忘れそうなので、メモ。

スナップショットテスト

  • vue コンポーネントをmount関数でマウントする
  • wrapper.elementがHTMLのDOM要素っぽいので、それをtoMatchSnapshotでアサートする。
test('renders correctly', () => {
  const wrapper = mount(Component)
  expect(wrapper.element).toMatchSnapshot()
})

試しに、snapshotディレクトリに作成された.snapファイルを少し修正して、再度テストを実行したら、テストがfailした。

なるほどー

vue用?のカスタムシリアライザライブラリもある

$ npm install --save-dev jest-serializer-vue

package.jsonのjestブロックに設定

"jest" : {
  "snapshotSerializers" : [ "jest-serializer-vue"]
}

うーん?スナップショットを再作成してみたけど、何も変わってない気がする。

多分、vueコンポーネントがシンプルだったからなのかな。結構DOM階層が多かったりすると、なにかきれいになったりするのかもしれない。

「インストール」の章はここまでかな。 mochaとかkarmaを使った場合の例もあったから、jestだけでいいや。

ガイド

はじめる

git clone https://github.com/vuejs/vue-test-utils-getting-started

マウンティングコンポーネント

import { mount } from '@vue/test-utils'
import Conter from './counter'

const wrapper = mount(Counter) // これでコンポーネントがマウントされる
const vm = wrapper.vm // vueインスタンスにアクセス

wrapper.html() // HTMLの文字列
wrapper.contains('button') // 要素が存在するかどうかの確認


//ユーザーインタラクションのシミュレーション
const button = wrapper.find('button') // findはセレクターを受け取るのかな?
button.trigger('click') // buttonのクリックイベントの発火

nextTickについて

  • VueはDOM更新をまとめて処理し、非同期に適用する
  • Vueが実際のDOM更新の実行を待つために普通はVue.nextTickが必要
  • 使い方を簡単にするために、vue-test-utilsは更新を同期的にて適用する
    • Vue.nextTickを使う必要はない
    • 非同期コールバックやプロミスの解決の操作のため、イベントループを明示的に進める必要があwる場合は、nextTickがまだ必要らしい
  • テストファイルでnextTickを使う必要がある場合、nextTickの内部でPromiseを使っているのでnextTick内で発生したエラーはテストランナーによって補足されない
it ('だめパターン', done => {
  Vue.nextTick(() => {
    expect(true).toBe(false) // これわざとfailするアサートしているのかな
    done()
  })
})

it("OKパターン:グローバルエラーハンドラを指定", done => {
  Vue.config.errorHandler = done // これ。
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it("OKパターン:nextTickのpromseを返す",  () => {
  return Vue.nextTIck().then(function() {
    expect(true).toBe(false)
  })
})

一般的なヒント

長いな。

何をテストするかを知る

  • 完全なラインベースのカバレッジを目指すことはおすすめされない
  • 内部をブラックボックスとして扱うことをおすすめ
    • 入力:ユーザーのやり取りやプロパティの変更
    • 出力:結果の描画やカスタムイベントの出力
  • Counterコンポーネントの場合
    • 入力:クリックをシミュレート
    • 出力:描画された出力が1つ増加したのかを検証

Shallow 描画

  • 単体テストはテスト対象のコンポーネントに焦点をあてる
  • vue-test-utilsshallowMountを使うと子コンポーネントをスタブによって描画せずにテスト対象コンポーネントをマウントできる
    • なんかうまく動かせなかったな。ただmountをshallowMountに変えただけだと
      • 使い方が違うのかも
    • shallowMountじゃなくてshallow関数に変わってたな

イベントの発行を検証する

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

// wrapper.emitted() で実行されたイベント情報が得られる
{ foo: [ [], [ 123 ] ] }

このemitted()で返されるイベント情報を使って、アサートする。

コンポーネントの状態を操作する

  • setData()関数を使って、vueインスタンスのdataをセットする
    • なんかうまくいかない、warrningがでる。
    • [Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option.
    • vueのバージョンでもう許可されなくなったとかそんなもんかな。
  • setProps()関数をつかって、プロパティを渡せる
    • これはできた。

プロパティをモックする

  • propertyを後からセットするんじゃなくて、mount時にわたすこともできる
mount(Component, {
  propsData: {
    hoge : "hoge"
 }
})

非同期動作のテスト

  • vue-test-utils は同期的にイベントをトリガします。従って、 Vue.nextTick() を実行する必要はない
    • コールバックやPromiseのようなコンポーネントの非同期動作をテストする場合のテクニックの紹介
  • axiosとか非同期APIを使う場合、普通にアサートしただけだと、アサート後に処理が実行される
    • 非同期処理を待つ必要がある
  • import flushPromises from 'flush-promises'を使って、可読性を上げることもできる。
    • await flushPromises することで、promiseを全部流せる
it("hoge test", done => {
  // 省略
  wrapper.vm.$nextTick(() => {
     expect(wrapper.vm.hoge).toBe("hoge")
     done()
  })
})

TypeScriptと一緒に使う、という章はチラ見した。

  • vueファイルを使うために、vue-jestを入れる
  • TypeScriptを使うために、ts-jestを入れる

くらいかな。

以上、で終わり!