【Rails7+Vue3】Vue.jsの自動テストを導入するまで

# はじめに
今回、Rails＋Vueで作っているWebアプリに、Vueの自動テストを導入することになり、試行錯誤しました。
せっかくなので、備忘録代わりに置いておきます。

「Vue 自動テスト」で検索すると、TypescriptやらVue-cliやらの話が結構引っかかりますが、今回は両方とも使っていません。

# 環境
rails 7.0.2.3
vue.js 3.2.37

# 参考ドキュメント
バージョンは古いですが、結局本家のドキュメントに一番助けてもらいました。
https://v1.test-utils.vuejs.org/ja/
ちなみに、新しいバージョンのドキュメントは2022.11.21現在、なぜか閲覧できません。。。

# 使うテストツール
### vue-test-utils
Vue標準のテストライブラリです。
### Jest
```vue-test-utils```で書いたテストを実行するテストランナです。
公式ドキュメントで一番上に書いてあったし、弊社ではフロントエンドテストの実績がなく、こだわりがないのでJestを選びました。
Jest以外のランナを使っても基本的には動かせるみたいです。

# Vue.jsの自動テスト
## セットアップ
基本的には、公式ドキュメントを読みながらひたすら必要なものをインストールしていきます。
ドキュメントは```npm```コマンドで書いてありますが、今回は開発環境の兼ね合いで、```yarn```コマンドを使ってインストールしていきました。

### 仮想ブラウザ環境のインストール
ライブラリがブラウザ環境に依存するので、テストを仮想ブラウザ環境で実行できるようにするものです。
```terminal
yarn add jsdom jsdom-global --dev
```
### vue-test-utilsとJestのインストール
```terminal
yarn add jest @vue/test-utils --dev
```
### vue-jestのインストール・設定
```.vue```ファイルをJestに扱ってもらうために、こちらもインストールします。
公式には```vue-jest```をインストールしてと書いてありますが、今回はvue3用のテストを書きたいので、```@vue/vue3-jest```をインストールします。
```terminal
yarn add @vue/vue3-jest --dev
```

```package.json```ファイルをルートディレクトリに作り、jestブロックを作って設定を書き入れます。
```json:package.json
{
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      // *.vue ファイルを処理するように指示する
      "vue"
    ],
    "transform": {
      // vue-jest で *.vue ファイルを処理する
      ".*\\.(vue)$": "vue-jest"
    }
  }
}
```
ちなみにjestの設定は後々膨らんでいくので、それが気になる場合は```package.json```にjestブロックを作るのをやめ、```jest.config.js```ファイルをルートディレクトリに作るのもおすすめです。私は今回、こちらの方法を取りました。
```js:jest.config.js
module.exports = {
  "moduleFileExtensions": [
      "js",
      "json",
      // *.vue ファイルを処理するように指示する
      "vue"
    ],
    "transform": {
      // vue3-jest で *.vue ファイルを処理する
      ".*\\.(vue)$": "@vue/vue3-jest"
    }
}
```
### babel-jestのインストール・設定
ES-modules構文などを使うために、```babel-jest```もインストールします。
```terminal
yarn add babel-jest --dev
```
インストール後、jsのテストファイルをbabelで処理できるように、以下の設定を```jest.config.js```（または```package.json```）に追記します。
```js:jest.config.js
module.exports = {
  // ...
  "transform": {
    '^.+\\.js$'  : '<rootDir>/node_modules/babel-jest',
  },
}
```

## 自動テストのディレクトリ構成
Jestの公式ドキュメントは、テスト対象のすぐ隣に```__tests__```ディレクトリを作ってテストを書くことを推奨していますが、強制ではないようです。
今回はRails+Vueのアプリに自動テストを導入するので、Railsがデフォルトで作る```test```ディレクトリに```javascript/__tests__```ディレクトリを作り、そこにテストを書いていくことにしました。

## カバレッジの設定
カバレッジを収集するための設定を書きます。
ここをちゃんと設定しないと、１ケースだけの自動テストに１分以上もかかることになるので、ちゃんとやります。

まずは、カバレッジを収集するために、下の設定を追記します。
```javascript:jest.config.js
module.exports = {
  // ...
  "collectCoverage": true,
  "coverageReporters": ["html", "text-summary"],
  "collectCoverageFrom": [
    "app/javascript/**/*.{js,vue}"
  ]
}
```
```collectCoverageFrom```オプションで、どのディレクトリのファイルのカバレッジを収集するかを設定しました。
今回は、```app/javascript```ディレクトリの下だけ収集できればいいのでそう書いていますが、公式のように```"**/*.{js,vue}"```と書けば、アプリ全体から収集してくれます。

次に下を追記します。
```javascript:jest.config.js
module.exports = {
  // ...
  "coverageDirectory": "coverage/_jest"
}
```
カバレッジのディレクトリ構成を指定するオプションです。今回は、Railsの```coverage```ディレクトリの下に```_jes```tディレクトリを置き、そこにカバレッジを収集することにしました。

## 自動テストを書く
テストを試験的に実行するために、コンポーネントの存在確認をするだけの簡単な自動テストを書いてみます。
```javascript:test/javascript/__tests__/component.test.js
import { mount } from '@vue/test-utils';
import Component from '(省略）/component';

const wrapper = mount(Component);

test('コンポーネントを描画できる', () => {
  // Componentを描画できる
  const comp = wrapper.findComponent(Component);
  expect(comp.exists()).toBe(true);
}
```

## 自動テストを実行
あとは実行するだけです。お疲れ様でした！
```terminal
yarn test
```

# テスト実行時に遭遇したエラーまとめ
ここからは、テストを実行するときに遭遇したエラーと、その解決法をまとめていきます。

## テストの実行コマンド違い
一番しょぼい間違いですが、テストの実行コマンドを勘違いしており、
```terminal
yarn test
```
ではなく
```terminal
yarn jest
```
で実行していました。

## SyntaxError: Cannot use import statement outside a module
原因はいろいろありましたが、まずは```package.json```に下を追記しました。
```json:package.json
{
  "scripts": {
    // ...
    "test": "jest"
  }
}
```
それでもうまくいかなく原因を探した結果、jestの```transform```オプションの中身を書き違えていることに気づきました。
```javascript:jest.config.js
module.exports = {
  // ....
  "transform": {
    ".*\\.(vue)$": "vue-jest", // 間違っている方
    ".*\\.(vue)$": "@vue/vue3-jest" // 正しい方
  }
}
```
ドキュメントが古いため勘違いしていましたが、今回はvue3に合わせたプリプロセッサをインストールしているので、こちらもそれに合わせた書き方をしないといけません。

この２つを直した結果、無事にこのエラーは乗り越えました。

## Error: Cannot find module '@vue/compiler-sfc’
```@vue/compiler-sfc```が必要らしいのでインストールして終わりです。
```terminal
yarn --cwd /ec-web add @vue/compiler-sfc --dev
```

## ReferenceError: document is not defined
```jest-environment-jsdom```をインストール・設定していないために起きるエラーみたいです。
まずはインストールします。
```terminal
yarn --cwd /ec-web add jest-environment-jsdom --dev
```
次に、テストファイルの一番上に以下を追記します。
```javascript:test/javascript/__tests__/component.test.js
/**
 * @jest-environment jsdom
 */

// ↑↑↑追記↑↑↑
import { mount } from '@vue/test-utils';
import Component from '(省略）/component';
// ...
```
https://github.com/testing-library/react-testing-library/issues/422#issuecomment-737905314

## ReferenceError: Vue is not defined
おそらく、vue3、jest v.28以上で起きるエラーのようです。```jest.config.js```に以下を追記します。
```javascript:jest.config.js
module.exports = {
  // ...
  "testEnvironmentOptions": {
    "customExportConditions": ["node", "node-addons"]
  }
}

```
https://stackoverflow.com/questions/72428323/jest-referenceerror-vue-is-not-defined

# テストを書くときに気をつけたかったエラー
上のエラーたちとは毛色が違うけれど、乗り越えるのに時間がかかったエラーも紹介します。
## Cannot find module 'Component' from '（ファイルパス）'
テストファイルでimportするときに、パスが正しくなくて遭遇したエラーです。
正しいパスで書くのが事情があってどうしても難しく、解決法としてaliasを設定しにいきました。
```javascript:jest.config.js
module.exports = {
  // ...
  "moduleNameMapper": {
     "^controllers(.*)$": "<rootDir>/（ファイルパス）"
  }
}
```

## isVueInstance is not a function
テストを実行できるか試しているときに、最初に試験的に書いたテストです。
```component.test.js
const wrapper = mount(Component)
expect(wrapper.isVueInstance()).toBe(true)
```
これの```isVueInstance```が使えないというエラーですが、公式ドキュメントに思いっきり
> isVueInstance は非推奨となり、将来のリリースで削除される予定です。

と書いてありました。
今回はバージョンが上がった方を使っているので、もう削除されて使えなくなったんだと思います。

## provide/injectのスタブ
provide/injectをスタブしたくて、公式ドキュメント通りに
```javascript
const wrapper = mount(Component, {
  provide: {
    foo() {}
  }
}
```
と書いたら、```this.foo() is not a function```エラーが出ました。
調べたところ、Vue3用では書き方が変わっているようで、正しくは下のように書くといいようです。
```javascript
const wrapper = mount(Component, {
  global: {
    provide: {
      foo() {}
    }
  }
}
```

https://qiita.com/KazukiMiyazato/items/2ab3a96df5a509e46c6b

## mountをする場所
最初はtestの中で```mount```をしていましたが、どうもテストのメソッドが思うように動いてくれませんでした。
```javascript
test 'componentを描画できる', () => {
  const wrapper = mount(Component);
  // ...
}
```
結果として、```mount```や```shallowMount```はtestの外でするといいみたいです。理由はよくわかりませんでした。
```javascript
const wrapper = mount(Component);
test 'componentを描画できる', () => {
  // ...
}
```

# おわりに
今回、初めて開発環境にフロントエンドテストを入れましたが、私自身が環境系に疎い上に社内に詳しい人がいなかったので、結構な時間がかかりました。
試行錯誤する時間は楽しかったし、良い経験になったと思います。

ところで、時間がかかった3分の1くらいは、バージョンアップ関連のエラーでした。
公式ドキュメント、早く正常にみられるようになると嬉しいです。。。

# 参照リンクまとめ
- [vue-test-utilsの公式ドキュメント](https://v1.test-utils.vuejs.org/ja/)
- [Jestの公式ドキュメント](https://jestjs.io/ja/docs/getting-started)
- [ReferenceError: document is not defined](https://github.com/testing-library/react-testing-library/issues/422#issuecomment-737905314)
- [jest ReferenceError: Vue is not defined](https://stackoverflow.com/questions/72428323/jest-referenceerror-vue-is-not-defined)
- [Vue3でprovide/injectを使用しているときのVue Test Utilsでのテストの書き方](https://qiita.com/KazukiMiyazato/items/2ab3a96df5a509e46c6b)