nuxt-typed-vuexを使ったページコンポーネントを（苦しまずに）単体テストするためのTips

## はじめに
これは以下の全てに当てはまる人向けの記事です。

1. Nuxtでも型推論の恩恵を得るために[nuxt-typed-vuex](https://github.com/danielroe/nuxt-typed-vuex)を導入している
2. [nuxt-typed-vuex](https://github.com/danielroe/nuxt-typed-vuex)を利用したページコンポーネントの単体テストを書こうとしている

元々はページコンポーネントが肥大化しそうだったため、リファクタリングを通じて処理の責任分割をしようと思ったのが発端です。
ただページコンポーネントが状態に応じて細かく表現を変える実装になってしまっており、「まずはページコンポーネントが意図通りに動くこと」を担保するため、ページコンポーネントの単体テストに手を出した次第です。

その際に幾つかの問題が発生したので、その内訳と対処法を残しておきます。

※ **リファクタリングの話、Vueのテスト方法、nuxt-typed-vuexが何ぞやという話はしませんのでご注意下さい**

## 前提
### 環境

| パッケージ | バージョン |
|---|---|
| nuxt | ^2.0.0 |
| nuxt-typed-vuex | ^0.1.18 |
| jest | ^24.1.0 |
| ts-jest | ^25.0.0 |
| babel-jest | ^24.1.0 |

### 構成
```
.
├── .babelrc
├── jest.config.js
├── test // テストファイル群
├── tsconfig.json
└── その他色々
```

```js:.babelrc
{
  env: {
    test: {
      presets: [
        [
          "@babel/preset-env",
          {
            targets: {
              node: "current"
            }
          }
        ]
      ]
    }
  }
}
```

```js:jest.config.js
module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest'
  },
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue'
  ]
}
```

パッケージも構成も、基本は`yarn create nuxt-app ...`を使ってプロジェクトを作成した状態と同じです。

## 発生した問題
### 1. `Unexpected token 'export'`
nuxt-typed-vuexの導入後、何も考えずに`yarn test`を実行したところ、以下の結果が出力されました。

```shell
$ yarn test
...
{path to repository}/node_modules/typed-vuex/lib/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){export { getStoreType } from './types/store';

SyntaxError: Unexpected token 'export'
...
```

(^ω^=^ω^) ?

どうやらJestに「CommonJSに従っていない構文（ここでは`export`）は解釈できないぞ！」と怒られているようです。
エラーログを見る限り、nuxt-typed-vuex配下のファイルがJestが解釈できる構文に変換されていない模様。

では変換するよう指示しなくては、と[JestのGitHubリポジトリにあるIssuesに掲載されている方法](https://github.com/facebook/jest/issues/2550#issuecomment-271614796)を試してみました。

```js:jest.config.js
module.exports = {
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
    '^~/(.*)$': '<rootDir>/$1',
    '^vue$': 'vue/dist/vue.common.js'
  },
  moduleFileExtensions: ['ts', 'js', 'vue', 'json'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest'
  },
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/components/**/*.vue',
    '<rootDir>/pages/**/*.vue'
  ],
  transformIgnorePatterns: ['<rootDir>/node_modules/(?!typed-vuex/lib)'] // ← 追加
}
```

追記した内容は「`node_modules/typed-vuex/lib`配下のファイルにのみ、Jest実行時に`babel-jest`を用いて変換処理を行う」という記述です。
デフォルトではnode_modules配下のファイルは変換されません。

しかしこれでも解決しませんでした。

#### 解決策
1. `jest.config.js`に`transformIgnorePatterns: ['<rootDir>/node_modules/(?!typed-vuex/lib)']`を追加する
2. `.babelrc`を`babel.config.js`にリネームする

1に関しては先程述べた通りです。
要はnuxt-typed-vuexのファイルをJestが解釈できる形に変換するよう指示しているだけですね。

しかし2…何だお前は…マジで…。

調べたところ、Babel（7.x）系の`.babelrc`に記述した設定はパッケージの境界を越えると無視されるらしいです。
そのためnode_modules配下のパッケージに対して`@babel/preset-env`の適用がなされなかったのだと思います。
対して`babel.config.js`は、プロジェクト配下に複数のパッケージがある場合でも同様のBabel設定を適用できるようなので、nuxt-typed-vuexに対しても`@babel/preset-env`が適用される、ということみたいです。

（っていうのを[Babelの公式サイト](https://babeljs.io/docs/en/config-files#6x-vs-7x-babelrc-loading)から読み取ったのですが、英語得意じゃないので誤っていたら指摘してください…）

ここまでで一旦、nuxt-typed-vuexを利用するコンポーネントのテストを「流すこと」はできるようになりました。
ただまだ終わりではありません。

### 2. コンポーネント側で利用する`this.$accessor`の中身が`undefined`になり、コンポーネントからStoreにアクセスできない

再び`yarn test`を実行したところ、以下の結果が出力されました。

```shell
TypeError: Cannot read property '{Vuexモジュールのアクセサ}' of undefined
```

ｵｲｨ…

コンポーネントからnuxt-typed-vuexを利用したVuexにアクセスするために以下の記述を書いているのですが、この`$accessor`が適切に呼び出されていないようです。

```ts:types/index.d.ts
import { accessorType } from '@/store'

declare module 'vue/types/vue' {
  interface Vue {
    $accessor: typeof accessorType
  }
}

declare module '@nuxt/types' {
  interface NuxtAppOptions {
    $accessor: typeof accessorType
  }
}
```

nuxt-typed-vuexの処理を追ってみると、typed-vuexというモジュールの`getAccessorFromStore()`という関数をnuxt-typed-vuexで呼び出し、アクセサを作成してプラグインとして差し込んでいる模様。
これはテストの時には呼び出されないため`$accessor`の内容にアクセスできず、躓いてしまっていたようです。

```js:nuxt-typed-vuex/lib/plugin.js
// eslint-disable-next-line
const { createStore } = require('<%= options.store %>');
// eslint-disable-next-line
const { getAccessorFromStore } = require('typed-vuex');
const storeAccessor = getAccessorFromStore(createStore());
export default async ({ store }, inject) => {
    inject('accessor', storeAccessor(store));
};
//# sourceMappingURL=plugin.js.map
```

#### 解決策
結論、`getAccessorFromStore()`関数を利用して作成したVuexへのアクセサを、テストコード側からVueインスタンスに渡して上げれば動きます。

```ts:PageComponent.spec.ts
import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'
import { getAccessorFromStore } from 'typed-vuex'
import Vuex, { Store } from 'vuex'
import PageComponent from '@/pages/index.vue'
import { store } from '@/store'

const localVue = createLocalVue()
localVue.use(Vuex)

let storeForTesting: Store<typeof store>
let wrapper: Wrapper<any>

beforeEach(() => {
  storeForTesting = new Store(store)
  wrapper = shallowMount(PageComponent, {
    { localVue },
    mocks: {
      $accessor: getAccessorFromStore(storeForTesting)(storeForTesting) // ここ
    }
  })
})

// 以下、テストコード
```

要はプラグインの実装と同じやり方ですね。

`jest.fn()`を利用してシンプルにモック化することも可能でしたが、今回は見送っています。
Vuexの値を利用したページコンポーネントの描画をテストしたかったためです。

またテストケースごとにモックの値を定義しておきたかったのですが、この時はやり方が分からず、愚直に`commit()`して値を設定しています。

## まとめ
以上2つの手段を用いることで、「nuxt-typed-vuexを使ったページコンポーネントを（苦しまずに）単体テストする」ことができると思います。
今回のやり方が適切かどうかは悩ましいところですが、少なくともテストを実行することはできるはずです。

同じ悩みを持つ人がもう苦しまなくて済みますように…。