Vue.js + Jest + TypeScriptのプロジェクトを作る 

Vue.js + TypeScriptのプロジェクトにJestを入れて、Vueコンポーネントのメソッドのテストをしたかったのですが、思いのほか詰まりまくったので記事にしておきます。

Vue Test Utilsという公式パッケージを使ってVue.jsとJestを連携させるのですが、Babel 7の変更にJestやvue-test-utilsが追いついていない感じがあって、ドキュメント通りにやっても上手くいきませんでした。Jestのために他のパッケージのバージョンを落とすのも嫌だったので、解決策を調べてみました。

サンプルコードはこちらです。
https://github.com/kecbigmt/vue-test-utils-ts-sample

# 筆者の環境
* OS: MacOS High Sierra 10.13.6
* node: 10.16.3
* vue-cli: 3.11.0
* yarn: 1.19.0

# 手順
以下の流れで説明していきます。

1. プロジェクト作成
2. 設定ファイルの編集
3. テストコードの追加
4. テスト実行

## プロジェクト作成

vue-cliでプロジェクトを作成します。手動でTSをONにします。あとはお好みで。

```bash:Terminal
$ vue create vue-test-utils-ts-sample

Vue CLI v3.11.0
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Linter
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: TSLint
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
```


## パッケージのインストール

必要なパッケージをインストールします。計8個のパッケージをまとめて入れています。

```bash:Terminal
$ yarn add --dev jest @types/jest @vue/test-utils vue-jest babel-jest ts-jest @babel/core babel-core@^7.0.0-bridge.0 @babel/preset-env
# npmの場合は`npm install -D ...`
```

以下、備忘録のためにも、何を何のためにインストールしたのかを解説します。
（最初の3つについては省略）

### xxx-jest
```bash:Terminal
$ yarn add --dev vue-jest babel-jest ts-jest
```

* `vue-jest`: .vueのファイルをJestに読ませるために必要
* `babel-jest`: ES2015のような新しめの文法で書いたスクリプトをJestに読ませるために必要
* `ts-jest`: .tsのファイルをJestに(ry

### @babel/core & babel-core
```bash:Terminal
$ yarn add --dev @babel/core babel-core@^7.0.0-bridge.0
```
`babel-jest`の依存関係のなかに`babel-core`が入っていています。しかし、Babel 7以降、babel-coreの最新バージョンは`@babel/core`として管理されています。

とはいえ、そのままだとbabel-jestが新しい名前を認識してくれないので、babel-jestに@babel/coreを渡すための繋ぎとして、`babel-core@^7.0.0-bridge.0`も一緒に入れます。

参考: [babel/babel-bridge: A placeholder package that bridges babel-core to @babel/core.](https://github.com/babel/babel-bridge)

### @babel/preset-env
```bash:Terminal
$ yarn add --dev @babel/preset-env
```
vue-cliで作成したプロジェクトでは、babelによるコンパイルを行うときのプリセットとして`@vue/app`というのを使ってるみたいですが、Jestが.vueファイルを解釈しようとするときに上手くいかないようです。

そのため、Babelの一般的なプリセットである`@babel/preset-env`をテストのときだけ使います。プリセットの切り替えについて設定ファイルに記述する部分は後ほど紹介します。

ちなみに、筆者の環境では@babel/preset-envを使わないとこんなエラーが出ました。

```bash:Terminal
$ yarn run test:unit
# 中略
/path/to/vue-test-utils-ts-sample/node_modules/@babel/runtime-corejs2/helpers/esm/classCallCheck.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){export default function _classCallCheck(instance, Constructor) {
                                                                                             ^^^^^^

    SyntaxError: Unexpected token export
```

## 設定ファイルの編集
パッケージのインストールが終わったら、設定ファイルを編集していきます。編集するファイルは`package.json`・`babel.config.js`・`tsconfig.json`の3つです。

### package.json

以下のように記述を追加します。テスト用のコマンドを追加したり、どのファイルのときに何を使って解釈するのかを設定したりしてます。

```json:package.json
{
  "scripts": {
    "test:unit": "jest"
  }
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      "vue"
    ],
    "transform": {
      ".*\\.(vue)$": "vue-jest",
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.tsx?$": "ts-jest"
    },
    "transformIgnorePatterns": [
      "/node_modules/(?!@babel/runtime-corejs2)"
    ]
  }
}
```

最後の`transformIgnorePatterns`の部分はエラーを回避するために入れました。Babel 7 + Jestの特有のエラーであるようですが、これがないと以下のように出てきます。
（@babel/preset-envが無いときと同じエラーです）

```bash:Terminal
$ yarn run test:unit
# 中略
/path/to/vue-test-utils-ts-sample/node_modules/@babel/runtime-corejs2/helpers/esm/classCallCheck.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){export default function _classCallCheck(instance, Constructor) {
                                                                                             ^^^^^^

    SyntaxError: Unexpected token export
```

参考: [あーありがち - 2019-02時点でJestの設定で最後までハマったもの](http://aligach.net/diary/20190228.html)

### babel.config.js

Babelのプリセットについて記述します。ここではテストのときだけ@babel/preset-envを使用するよう記述します。

```js:babel.config.js
module.exports = {
  presets: [
    '@vue/app'
  ],
  env: {
    test: {
      presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
    }
  }
}
```

参考: [Testing Single-File Components with Jest | Vue Test Utils](https://vue-test-utils.vuejs.org/guides/testing-single-file-components-with-jest.html#configuring-babel-for-jest)

### tsconfig.js

最後にこのファイルです。テストコードのなかでJestの関数を使えるようにするために追加します。

```json:tsconfig.json
{
  "compilerOptions": {
    "types": [
      "jest",
      //..
    ],
    //..
  },
  //..
}
```

## テストコードの追加
続いて、テストコードを追加します。ここでは、プロジェクト作成時に自動で生成される`src/component/HelloWorld.vue`のテストを追加します。

HelloWorld.vueと同じ階層に `HelloWorld.spec.ts` というファイルを新しく作成します。

ファイルの中には以下のように記述します。

```ts:HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils';
import HelloWorld from './HelloWorld.vue';

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message';
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg },
    });
    expect(wrapper.text()).toMatch(msg);
  });
});
```

（[公式ドキュメント](https://vue-test-utils.vuejs.org/guides/testing-single-file-components-with-jest.html#placing-test-files)ではテスト対象ファイルと同じ階層に`__tests__`というディレクトリを作って、その中にテストファイルを格納することが推奨されています。ですが、自分なりの配置をしても構わないとも記載されています）

テストの内容を軽く説明します。

```ts
const msg = 'new message';
const wrapper = shallowMount(HelloWorld, {
  propsData: { msg },
});
```
まず、`msg`というpropに"new message"という値を渡した上で、HelloWorldコンポーネントをマウント・レンダリングします。

`shallowMount()`は@vue/test-utilsの関数。浅くマウント（コンポーネントの中のコンポーネントまでは生成されない）され、レンダリングされたVueコンポーネントを含むWrapperを返します。

shallowMount()やその他の関数については[API | Vue Test Utils](https://vue-test-utils.vuejs.org/api/#shallowmount)で説明されています。

```ts
expect(wrapper.text()).toMatch(msg);
```

WrapperはVueコンポーネントに関するいろいろな情報を返してくれます。`wrapper.text()`はコンポーネントに含まれるDOM要素内の文字列（`<p>この部分</p>`）をすべて繋げて返します。

JestのtoMatch()メソッドを使って、これに"new message"が含まれているかをテストしています。

Wrapperについては[Wrapper | Vue Test Utils](https://vue-test-utils.vuejs.org/api/wrapper/)が参考になります。

## テスト実行

用意が整ったので、いよいよテストを実行します。package.jsonで設定した通り、`test:unit`のコマンドを使用できるようになっています。

```bash:Terminal
$ yarn run test:unit # npmの場合は`npm run test:unit`

yarn run v1.19.0
$ jest
 PASS  src/components/HelloWorld.spec.ts
  HelloWorld.vue
    ✓ renders props.msg when passed (19ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.842s
Ran all test suites.
✨  Done in 2.87s.
```

以上です。テストが動くようになりました。

サンプルコードはこちらです。
https://github.com/kecbigmt/vue-test-utils-ts-sample

# 参考
* [Using with TypeScript | Vue Test Utils](https://vue-test-utils.vuejs.org/guides/using-with-typescript.html)
* [Testing Single-File Components with Jest | Vue Test Utils](https://vue-test-utils.vuejs.org/guides/testing-single-file-components-with-jest.html)
* [babel/babel-bridge: A placeholder package that bridges babel-core to @babel/core.](https://github.com/babel/babel-bridge)
* [あーありがち - 2019-02時点でJestの設定で最後までハマったもの](http://aligach.net/diary/20190228.html)