LoginSignup
4
2

More than 3 years have passed since last update.

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

Posted at

はじめに

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

  1. Nuxtでも型推論の恩恵を得るためにnuxt-typed-vuexを導入している
  2. 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
└── その他色々
.babelrc
{
  env: {
    test: {
      presets: [
        [
          "@babel/preset-env",
          {
            targets: {
              node: "current"
            }
          }
        ]
      ]
    }
  }
}
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を実行したところ、以下の結果が出力されました。

$ 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に掲載されている方法を試してみました。

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.jstransformIgnorePatterns: ['<rootDir>/node_modules/(?!typed-vuex/lib)']を追加する
  2. .babelrcbabel.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が適切に呼び出されていないようです。

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の内容にアクセスできず、躓いてしまっていたようです。

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インスタンスに渡して上げれば動きます。

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を使ったページコンポーネントを(苦しまずに)単体テストする」ことができると思います。
今回のやり方が適切かどうかは悩ましいところですが、少なくともテストを実行することはできるはずです。

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2