Vue3 コンポーネントのテスト

Componentの単体テストは書くべきでしょうか?UIのテストは割りに合わない、ロジックはコンポーネントの外のモジュールに記述している(特にcomposition APIでは)などの理由から、コンポーネントに対するテストは書かない選択肢もあるでしょう。

とはいえ、やはりテストが書いてあると変更を加えた際に安心できるものです。コンポーネントのテストをするのフレームワークとしてJestvue-test-utils 2を利用します。Vue2系をターゲットにしているvue-test-utils 1とは一部APIが異なります。

なにをテストするのか

コンポーネントのテストは、詳細には触れずに入力と出力に着目してテストを行います。例えば、propsとして渡したデータが適切DOMに描画されるかなどがテストケースとして挙げられます。

入力、出力はそれぞれ以下の分類があることが考えられます。

  • 入力
    • props
    • 算出プロパティ
    • フォーム入力、クリックなどのユーザーイベント
    • store(Vuexなど)の非同期アクション
  • 出力
    • DOMの描画
    • emit
    • storeの状態の変更

テストを書いてみる

コンポーネントの実装

それでは実際にテストを書いていきます。 「Googleでログイン・Twitterでログインなどのボタンをクリックした際にユーザー認証されてホーム画面へ遷移する」というシナリオを想定しています。下記がコンポーネントの実装です。

# LoginButtons.vue
<template>
  <login-button @click="clickGoogle">Googleでログイン</login-button>
  <login-button @click="clickTwitter">Twitterでログイン</login-button>
</template>

<script>
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'
import LoginButton from '@/components/molecules/LoginButton.vue'
import { signInWithGoogle, signInWithTwitter } from '@/composables/use-auth'

export default defineComponent({
  components: {
    LoginButton
  },
  setup () {
    const router = useRouter()

    const clickGoogle = async () => {
      const user = await signInWithGoogle()
      if (!user) return

      router.push('/home')
    }

    const clickTwitter = async () => {
      const user = await signInWithTwitter()
      if (!user) return

      router.push('/home')
    }
    return {
      clickGoogle,
      clickTwitter
    }
  }
})
</script>

テストファイルの作成

テストファイルは、testフォルダ配下に配置することにします。test/components/LoginButtons.spec.tsを作成します。

import { shallowMount } from '@vue/test-utils'
import LoginButtons from '@/components/organism/LoginButtons.vue'
import LoignButton from '@/components/molecules/LoginButton.vue'
import { signInWithGoogle } from '@/composables/use-auth'
import flushPromises from 'flush-promises'

const mockPush = jest.fn()
jest.mock('vue-router', () => ({
  useRouter: () => {
    return {
      push: mockPush
    }
  }
}))

let mockError: boolean
jest.mock('@/composables/use-auth', () => ({
  signInWithGoogle: jest.fn(() => {
    if (mockError) {
      return Promise.resolve(null)
    }
    const user = {
      uid: 'uid',
      displayName: 'name',
      photoURL: 'photo'
    }
    return Promise.resolve(user)
  })
}))

describe('LoginButtons.vue', () => {
  const wrapper = shallowMount(LoginButtons)
  const button = wrapper.findAllComponents(LoignButton)

  test('Googleでログインボタンを押すと、signInWithGoogleが呼ばれホームページ遷移する', async () => {
    button[0].trigger('click')
    await flushPromises()
    expect(signInWithGoogle).toHaveBeenCalled()

    expect(mockPush).toHaveBeenCalledWith('/home')
  })

  test('Googeleでログインボタンを押した際に、ログインに失敗した場合ページ遷移はしない', async () => {
    mockError = true
    button[0].trigger('click')
    await flushPromises()

    expect(mockPush).toHaveBeenCalledTimes(0)
  })
})

ざっとこのようなテストになるはずです。詳しく見ていきましょう。

外部モジュールをモック化する

実際に認証をする処理など副作用が発生する関数は、テストしづらくなってしますのですべてモック化します。また、モック化をする際に処理をjest.fn()に置き換えることでその関数がどのように呼ばれたのかをテストすることができます。

const mockPush = jest.fn()
jest.mock('vue-router', () => ({
  useRouter: () => {
    return {
      push: mockPush
    }
  }
}))

let mockError: boolean
jest.mock('@/composables/use-auth', () => ({
  signInWithGoogle: jest.fn(() => {
    if (mockError) {
      return Promise.resolve(null)
    }
    const user = {
      uid: 'uid',
      displayName: 'name',
      photoURL: 'photo'
    }
    return Promise.resolve(user)
  })
}))

jest.mock()はモック化したいモジュールを第一引数に渡します。signInWithGoogleをモック化する際には、mockError変数によって成功時、失敗時をテストできるようにします。

コンポーネントをレンダリングして要素を取得する

コンポーネントをレンダリングするためには、mountまたはshallowMountを使います。mountはコンポーネントのすべての要素をレンダリングしますが、shallowMountは子コンポーネントをスタブ化してレンダリングします。

mountshallowMountはレンダリングしたいコンポーネントを引数に受け取り、wrapperオブジェクトを返します。

レンダリングされた要素を取得するには、wrapperオブジェクトのfindfindAllfindComponentfindAllComponentsを利用します。

findfindAllは取得したい要素をquerySelectorで指定します。 findComponentfindAllComponentsはコンポーネントインスタンスにより要素を検索します。 Allという名前がついているメソッドは、見つかった要素すべてを返します。

const wrapper = shallowMount(LoginButtons)
const buttons = wrapper.find(LoignButton)

ボタンクリックを発生させる

取得した要素のイベントを発生させるためには、triggerメソッドを利用します。triggerメソッドの引数には発生させたいイベント名を指定します。 Googleでログインボタンはたしか1つ目の要素でしたので、buttons[0]で要素を取得しています。

  test('Googleでログインボタンを押すと、signInWithGoogleが呼ばれホームページ遷移する', async () => {
    mockError = false
    buttons[0].trigger('click')
    await flushPromises()
    expect(signInWithGoogle).toHaveBeenCalled()

    expect(mockPush).toHaveBeenCalledWith('/home')
  })

Googleでログインボタンをクリックした際に発火するclickGoogle関数は、非同期に実行されます。後続のアサーションまでにプロミスが解決していない場合には、テストが失敗してしまう可能性があります。

そのため、flush-promisesを利用して要素をクリックしてからすべてのプロミスをすぐに解決させます。

ここでは、signInWithGoogleが呼ばれて、router.push()/homeという引数で呼ばれていることをテストしています。

終わりに

ここでは、クリックイベントの処理という簡単なテストの実行方法のみを取り上げました。propsを与える、DOMの変更をテストする、emitの発生をテストするなど他にも様々なユースケースが考えられます。そのような要求に対してもわりと簡単にテストを実行することができるので、リンクを参考に実施してみてください。

参考

Vue Test Utils(2.0.0-beta.10) Vue Testing Handbook

この記事をシェアする
Twitterで共有
Hatena

関連記事