はじめに
これは以下の全てに当てはまる人向けの記事です。
- Nuxtでも型推論の恩恵を得るためにnuxt-typed-vuexを導入している
- 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
└── その他色々
{
env: {
test: {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
]
]
}
}
}
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
を実行したところ、以下の結果が出力されました。
$ 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に掲載されている方法を試してみました。
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配下のファイルは変換されません。
しかしこれでも解決しませんでした。
解決策
-
jest.config.js
にtransformIgnorePatterns: ['<rootDir>/node_modules/(?!typed-vuex/lib)']
を追加する -
.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の公式サイトから読み取ったのですが、英語得意じゃないので誤っていたら指摘してください…)
ここまでで一旦、nuxt-typed-vuexを利用するコンポーネントのテストを「流すこと」はできるようになりました。
ただまだ終わりではありません。
2. コンポーネント側で利用するthis.$accessor
の中身がundefined
になり、コンポーネントからStoreにアクセスできない
再びyarn test
を実行したところ、以下の結果が出力されました。
TypeError: Cannot read property '{Vuexモジュールのアクセサ}' of undefined
オイィ…
コンポーネントからnuxt-typed-vuexを利用したVuexにアクセスするために以下の記述を書いているのですが、この$accessor
が適切に呼び出されていないようです。
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
の内容にアクセスできず、躓いてしまっていたようです。
// 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インスタンスに渡して上げれば動きます。
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を使ったページコンポーネントを(苦しまずに)単体テストする」ことができると思います。
今回のやり方が適切かどうかは悩ましいところですが、少なくともテストを実行することはできるはずです。
同じ悩みを持つ人がもう苦しまなくて済みますように…。