ラベル vuex の投稿を表示しています。 すべての投稿を表示
ラベル vuex の投稿を表示しています。 すべての投稿を表示

2019年6月13日木曜日

JestでVuex.Storeのテストを行う

NuxtでTypeScriptを使うときのVeux.storeの設定方法で作成したプロジェクトに Jest でのユニットテストをつけていきます。

できあがったものは以下のリポジトリになります。
https://github.com/TAC/nuxt-vuex-typescript-jest-example

Jestの追加

Nuxt を新規作成時のオプションでテストフレームワークに Jest を選択すると自動で追加されますが、それらを手動で追加してみます。

yarn add -D jest vue-jest babel-jest ts-jest eslint-plugin-jest @vue/test-utils babel-core@^7.0.0-bridge.0

Babelのバージョンが7になっているのでbabel-core@^7.0.0-bridge.0 が必要になります。
Vuex.StoreTypeScriptなのでts-jestも追加します。

Jestの設定

jest.config.js.babelrc を以下の内容で追加します。

app/jest.config.ts

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

.babelrc

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

テストの作成

コンポーネントのテスト

まずはコンポーネントのテストから。

import Vuex from 'vuex'
import { cloneDeep } from 'lodash'
import { createLocalVue, mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
import counter from '@/store/modules/counter.ts'

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

describe('components/Counter.vue', () => {
  let wrapper
  const initValue = 10
  beforeEach(() => {
    // create store
    const store = new Vuex.Store({
      'modules': {
        'modules/counter': cloneDeep(counter)
      }
    })

    // initialize store
    store.commit('modules/counter/SET_VALUE', initValue)

    // mount Vue Component
    wrapper = mount(Counter, {
      store: store,
      localVue
    })
  })

  describe('template', () => {
    test('snapshot correctly', () => {
      expect(wrapper.element).toMatchSnapshot()
    })
  })

  describe('script', () => {
    test('get counter value correctly', () => {
      expect(wrapper.vm.counter).toBe(initValue)
    })
    test('increment button correctly', () => {
      wrapper.find('[name="increment"]').trigger('click')
      expect(wrapper.vm.counter).toBe(initValue + 1)
    })
    test('decrement button correctly', () => {
      wrapper.find('[name="decrement"]').trigger('click')
      expect(wrapper.vm.counter).toBe(initValue - 1)
    })
  })
})

@vue/test-utilscreateLocalVueを使ってローカルコピーを作成しておきます。
Vuex.Storeには使用するstoreの定義をmodulesの該当パスにcloneDeepでコピーしてあげます。
そうすることで他のテスト時に値が汚染されなくなります。
あとは好きなテストを記述していきます。
wrapper.find()にはCSSセレクターを指定できるので、テスト対象のエレメントなどが一意になるようにnameclassを設定しておくと探しやすくなります。

Veux.Storeのテスト

次はstoreのテストです。

import Vuex from 'vuex'
import { cloneDeep } from 'lodash'
import { createLocalVue } from '@vue/test-utils'
import counter from '@/store/modules/counter.ts'

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

describe('store/modules/counter.ts', () => {
  let store
  const initValue = 10
  beforeEach(() => {
    // create store
    store = new Vuex.Store({
      'modules': {
        'modules/counter': cloneDeep(counter)
      }
    })

    // initialize store
    store.commit('modules/counter/SET_VALUE', initValue)
  })

  describe('getters', () => {
    test('get value correctly', () => {
      expect(store.getters['modules/counter/value']).toBe(initValue)
    })
  })

  describe('actions', () => {
    test('increment correctly', () => {
      const addValue = 1
      store.dispatch('modules/counter/increment', addValue)
      expect(store.getters['modules/counter/value']).toBe(initValue + addValue)
    })
    test('decrement correctly', () => {
      const subValue = 1
      store.dispatch('modules/counter/decrement', subValue)
      expect(store.getters['modules/counter/value']).toBe(initValue - subValue)
    })
  })
})

ほぼコンポーネントと同じですが、作成したstoreを使用してテストしたいactionsなどを実行していく形になります。

package.jsonに以下のスクリプトを追加します。

    "test": "jest",
    "test:coverage": "jest --coverage",

これで準備が整ったので、yarn testを実行すると以下のような結果が得られると思います。

PS C:\work\repos\nuxt\jest\app> yarn test
yarn run v1.15.2
$ jest
$ C:\work\repos\nuxt\jest\app\node_modules\.bin\jest
 PASS  tests/store/modules/counter.test.js
 PASS  tests/components/Counter.test.js

Test Suites: 2 passed, 2 total
Tests:       7 passed, 7 total
Snapshots:   1 passed, 1 total
Time:        3.831s
Ran all test suites.
Done in 5.54s.

テストカバレッジも見たい時はtest:coverageを実行します。

PS C:\work\repos\nuxt\jest\app> yarn test:coverage
yarn run v1.15.2
$ jest --coverage
$ C:\work\repos\nuxt\jest\app\node_modules\.bin\jest --coverage
 PASS  tests/store/modules/counter.test.js
 PASS  tests/components/Counter.test.js
---------------|----------|----------|----------|----------|-------------------|
File           |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------|----------|----------|----------|----------|-------------------|
All files      |    65.38 |      100 |     87.5 |    65.22 |                   |
 components    |    71.43 |      100 |      100 |    71.43 |                   |
  Counter.vue  |      100 |      100 |      100 |      100 |                   |
  Logo.vue     |        0 |      100 |      100 |        0 |             10,13 |
 pages         |        0 |      100 |      100 |        0 |                   |
  index.vue    |        0 |      100 |      100 |        0 |         8,9,10,18 |
 store         |        0 |      100 |        0 |        0 |                   |
  index.ts     |        0 |      100 |        0 |        0 |               1,3 |
 store/modules |      100 |      100 |      100 |      100 |                   |
  counter.ts   |      100 |      100 |      100 |      100 |                   |
---------------|----------|----------|----------|----------|-------------------|

Test Suites: 2 passed, 2 total
Tests:       7 passed, 7 total
Snapshots:   1 passed, 1 total
Time:        10.346s
Ran all test suites.
Done in 11.68s.

Written with StackEdit.

2019年6月4日火曜日

NuxtでTypeScriptを使うときのVeux.storeの設定方法

NuxtTypeScript で構築した時に Vuex.store の構成をどうしたらいいかを考えてみました。

結論としては vuex-class-component を使用してモジュールモードで構成する、という感じになりました。

完成したプロジェクトは以下のリポジトリになります。
https://github.com/TAC/nuxt-vuex-typescript-example

簡単なカウンターを作成しました。

環境

- version
OS Windows10
Node.js v8.15.0
Nuxt.js v2.7.1

どのモジュールがいいのか?

Vuex.store を簡潔にかけるようにするモジュールはいくつかあって、以下の2つが主に使われてるようでした。

vuex-class-component
https://github.com/michaelolof/vuex-class-component

vuex-module-decorators
https://github.com/championswimmer/vuex-module-decorators

Nuxt では store ディレクトリ以下にファイルを配置すれば自動で Vuex のモジュールモードで構築してくれます。

ただし、 store/index.ts が関数を返すとクラシックモードになり、その恩恵を受けることができません。
また、各モジュールのファイルも関数ではなくオブジェクトを返す必要があり、それができるのが vuex-class-component でした。
また、Nuxt.jsv3.0.0 ではクラシックモードが非対応になるようなので、それを考慮に入れての選択になります。

ディレクトリ構成

以下のような構成を考えてみました。

src/store/index.ts
  +- modules/
      +- counter.ts

modules に機能毎に分けたモジュールを作成していく感じです。
models とか作ってDBのテーブル毎などに分けたモジュールを作成してもいいかもしれない。

counter.ts

デコレータを使って以下のように記述します。

import {
  VuexModule,
  Module,
  action,
  getter,
  mutation
} from 'vuex-class-component'

interface Counter {
  value: number
}

@Module({ namespacedPath: 'modules/counter/', target: 'nuxt' })
export class CounterStore extends VuexModule implements Counter {
  @getter value = 0

  @mutation
  public SET_VALUE(value: number) {
    this.value = value
  }

  @action()
  public increment(value: number) {
    this.SET_VALUE(this.value + value)
  }

  @action()
  public decrement(value: number) {
    this.SET_VALUE(this.value - value)
  }
}

export default CounterStore.ExtractVuexModule(CounterStore)

ポイントは2つあって、1つは @Module のオプションに namespacedPathtarget:'nuxt' を指定するところです。
もう1つは、最後の行で ExtractVuexModule を使ってオブジェクト形式で export してあげるところです。
これで Vuex.store のモジュールモードに対応できました。

index.ts

index.ts もデコレータを使って記述しようとしたのですが、 nuxtServerInit の定義がどうしてもできなかったので、デコレータを使わずに記述します。

export const actions = {
  nuxtServerInit: ({ commit }) => {
    commit('modules/counter/SET_VALUE', 20)
  }
}

ここでは modules/counter の初期化を行ってみました。

呼び出すComponent

components/Counter.vue を作成します。

<template lang="pug">
  div [counter]
    div {{ counter }}
    div
      button(@click="increment") +
      button(@click="decrement") -
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class Counter extends Vue {
  get counter() {
    return this.$store.getters['modules/counter/value']
  }

  public increment() {
    this.$store.dispatch('modules/counter/increment', 1)
  }
  public decrement() {
    this.$store.dispatch('modules/counter/decrement', 1)
  }
}
</script>

呼び出す箇所は通常の使い方と同じです。

これでごちゃごちゃしがちな store 周りをスッキリとかけるようになったかと思います。

Written with StackEdit.