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

2019年11月23日土曜日

NuxtでFirestoreを利用してデータを保存する

Firestoreからデータを取得して表示するサンプルを作成します。
Jest でテストもします。

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

Firestoreに接続する

まずはFirestoreへ接続します。
Firebaseのアカウント作成などは他のサイトなどで確認して下さい。
接続情報は.env/development.jsに記載して、nuxt.config.tsで読み込むようにします。

.env/development.js

module.exports = {
  apiKey: "<your apiKey>",
  authDomain: "<your authDomain>",
  databaseURL: "<your databaseURL>",
  projectId: "<your projectId>",
  storageBucket: "<your storageBucket>",
  messagingSenderId: "<your api messagingSenderId>"
}

app/nuxt.config.ts

import NuxtConfiguration from '@nuxt/config'
import pkg from './package.json'
import envSet from '../.env/environment.js'

const nuxtConfig: NuxtConfiguration = {
  mode: 'universal',
  srcDir: 'src',

  env: envSet,
  ...
}

設定値はprocess.env.apiKeyという形式で利用できます。
次にFirebaseFirestore の初期設定を行うプラグインを作成します。

app/src/plugins/firebase.ts

import firebase from 'firebase/app'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: process.env.apiKey,
    authDomain: process.env.authDomain,
    databaseURL: process.env.databaseURL,
    projectId: process.env.projectId,
    storageBucket: process.env.storageBucket,
    messagingSenderId: process.env.messagingSenderId
  })
}

export default firebase

ここまでは 前回のAuthenticationの実装 でやりました。
Firestore へ接続するプラグインは以下のように記述します。

app/src/plugins/firestore.ts

import firebase from '@/plugins/firebase'
import 'firebase/firestore'

const db = firebase.firestore()

export default db

修正箇所

ユーザのニックネームを Firestore へ保存します。
users コレクションを作成して、ドキュメントIDには AuthenticationユーザーUID を利用します。
認証データをそのまま保存していた store/models/users.ts を変更していきます。

store/models/users.ts

import {
  VuexModule,
  Module,
  getter,
  mutation,
  action,
  getRawActionContext
} from 'vuex-class-component'
import firebase from '@/plugins/firebase'
import db from '@/plugins/firestore'
import Users from '@/types/store/models/users'

const timestamp = firebase.firestore.FieldValue.serverTimestamp()

@Module({ namespacedPath: 'models/users/', target: 'nuxt' })
class Store extends VuexModule {
  @getter user: Users = {}

  get isAuthenticated() {
    return !!this.user.id
  }

  @mutation
  public SET_USER(payload: Users) {
    this.user = payload
  }

  @mutation
  public UNSET_USER() {
    this.user = {}
  }

  @action({ mode: 'raw' })
  public async set(uid: string) {
    if (uid) {
      const ref = db.collection('users').doc(uid)
      const doc = await ref.get()
      if (doc.exists) {
        const data: Users = {
          id: doc.id,
          nickName: doc.data()!.nickName,
          createdAt: doc.data()!.createdAt,
          updatedAt: doc.data()!.updatedAt
        }
        const context = getRawActionContext(this)
        context.commit('SET_USER', data)
      }
    }
  }

  @action({ mode: 'raw' })
  public async unset() {
    const context = getRawActionContext(this)
    context.commit('UNSET_USER')
  }

  @action({ mode: 'raw' })
  public async create(uid: string) {
    if (uid) {
      const ref = db.collection('users').doc(uid)
      const doc = await ref.get()
      if (!doc.exists) {
        const nickName = ''
        await ref.set({
          nickName,
          createdAt: timestamp,
          updatedAt: timestamp
        })

        const data: Users = {
          id: uid,
          nickName
        }
        const context = getRawActionContext(this)
        context.commit('SET_USER', data)
      }
    }
  }

  @action({ mode: 'raw' })
  public async update(user: Users) {
    const ref = db.collection('users').doc(user.id)
    const doc = await ref.get()
    if (doc.exists) {
      await ref.update({
        nickName: user.nickName,
        updatedAt: timestamp
      })
      const context = getRawActionContext(this)
      context.commit('SET_USER', user)
    }
  }
}

export default Store.ExtractVuexModule(Store)

以前は mutation しかありませんでしたが、 action を追加して、そこでユーザデータの登録・作成を行うようにしています。

認証のサインイン時の処理も修正します。

store/modules/auth.ts

  @action({ mode: 'raw' })
  public async signIn(provider) {
    const context = getRawActionContext(this)
    context.commit('models/users/UNSET_USER', null, { root: true })
    await firebase
      .auth()
      .signInWithPopup(provider)
      .then(result => {
        if (result && result.user && result.additionalUserInfo) {
          if (result.additionalUserInfo.isNewUser) {
            // new user
            context.dispatch('models/users/create', result.user.uid, {
              root: true
            })
          } else {
            // exists user
            context.dispatch('models/users/set', result.user.uid, {
              root: true
            })
          }
        }
      })
      .catch(error => {
        console.error(error)
        throw error
      })
  }

result.additionalUserInfo.isNewUser をみて新規ユーザかどうかを判定して、users コレクションへの登録処理を実行しています。
一度認証してしまっていると上記の値が false になってしまうので、動作確認する場合は新しいアカウントか Firebase Console で認証情報を削除すると users コレクションが登録されると思います。

テストの作成

認証処理の時にも使った firebase-mock を使ってテストケースを作っていきます。

app/tests/store/models/users.test.js

import db from '@/plugins/firestore'
...
describe('store/models/users.ts', () => {
  beforeAll(() => {
    db.autoFlush()
  })

  beforeEach(async () => {
    // mock data
    user = {
      nickName: 'alice',
      createdAt: '2019-10-26 00:00:00',
      updatedAt: '2019-10-26 00:00:00'
    }

    await db
      .collection('users')
      .doc('alice')
      .set(user)
  })

plugins/firestoreimport を追加してます。
前回の設定firestore の呼び出しはモックになっています。
beforeAll でFirestoreへの処理がすぐ反映されるように autoFlush() を実行しておきます。
beforeEach でモックデータを登録しています。
モックなので実際にFirestoreには登録されません。

app/tests/store/models/users.test.js

  describe('actions', () => {
    test('set', async () => {
      await store.dispatch('models/users/set', 'alice')
      expect(store.getters['models/users/user']).toMatchObject(user)
    })

    test('unset', async () => {
      await store.dispatch('models/users/unset')
      expect(store.getters['models/users/user']).toMatchObject({})
    })

    test('create', async () => {
      await store.dispatch('models/users/create', 'bob')
      const checkData = {
        id: 'bob',
        nickName: ''
      }
      expect(store.getters['models/users/user']).toMatchObject(checkData)
    })

    test('update', async () => {
      const updateData = {
        id: 'alice',
        nickName: 'test'
      }
      await store.dispatch('models/users/update', updateData)
      const data = store.getters['models/users/user']
      expect(data).toMatchObject(updateData)
      expect(data.updatedAt).not.toBe(user.updatedAt)
    })
  })

追加した action メソッドのテストを作成します。
なお、gettersmutation は動作を変えていないので、修正しなくても正常にテストを通過するはずです。

以上、簡単にですが Firestore への接続とテスト方法の紹介でした!


Written with StackEdit.

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.