Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

はじめに

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

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

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

dadayama
フロント寄りの人間です。 最近は情報設計やらUIの学びを深めています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away