Vue3で保守性の高いテストコード作成方法を解説!Vue3エンジニアが実装とテストの疎結合実装方法を解説!

Vue3で保守性の高いテストコード作成方法を解説!Vue3エンジニアが実装とテストの疎結合実装方法を解説!

こんにちは!

今回は Vue3 でのコンポーネントのテストコードの作成方法を紹介します!

テストコードを書くことでチーム開発はもちろん、個人開発においてもコードをリファクタリングしたり、バグを発見したりする際に重要な役割を果たします。

Vue3 から新たに登場した Compostion API を利用することで、より保守性の高いコンポーネントを定義することができます。

コンポーネントをテストする場合、一般的にはテストコードを実装からできる限り切り離すように作成します。 理想的なのはコンポーネント内のロジックなどを考慮せずに実施するブラックボックス的なテストを書くことです。

はじめに

本記事では Vue3 でのテストコードの作成方法について解説しております。

Vue2 では Vue.extend でコンポーネントを定義する Options API で書くのが主流でしたが、 Vue3 では新たに Composition API と呼ばれるものがリリースされました。

Vue2 のプロジェクトで Composition API を使えるようにすることも可能ですが、 今回は Vue3 で動かすことを前提として解説します。

Vue3 に限らず、Web アプリケーションのフロントエンドのテストには、一般的に下記の3種類があります。

  • 単体テスト
  • コンポーネントテスト
  • E2E(End-to-End)テスト

単体テスト

単体テストは、ビジネスロジックや汎用的な処理が意図した通りに動作するかをテストします。
単体テストを行うことで、新しい機能が構築されたり、コードがリファクタリングされてもアプリケーションが機能的で安定した状態を保つことができます。
Vue ではコンポーネント内の個々のロジックのテストをするイメージです。

Vue3 で使用される単体テストフレームワークは JestMocha などがあります。

コンポーネントテスト

コンポーネントテストは、実装した Component が意図した通りのレンダリング・振る舞いを行なっているかをテストします。
Vue3 で使用されるコンポーネントテストライブラリは Vue Testing LibraryVue Test Utils などがあります。

Vue Testing LibraryVue Test Utils を抽象化したものなので、 Vue で初めてテストを書く場合は Vue Testing Library を使用することをお勧めします。

E2E テスト

E2E テストは、実際に利用するユーザーの操作シナリオを作成・自動実行し、Web ブラウザを通した一通りの操作が行えるかをテストします。
フロントエンドのコードだけではなく、バックエンドのコードやインフラ周りも含まれます。
こちらは実際の挙動に近いテストのため、今回の記事では解説は行いません。

※代表的なツールとしては Cypress などがあります。

想定する読者

  • VueJS でテストコードを書いたことがない人
  • Vue2 ではテストコードを書いたことがあるけど、Vue3 では書いたことがない人

Vue3 でテスト環境を構築する

今回は vue-cli でプロジェクトを作成していきます。

$ vue create hello-world
Vue CLI v4.5.13
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 3.x
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? No
? Pick a linter / formatter config: Basic
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

Manually select features を選択して、TypeScript、テストフレームワークなどを選択します。

プロジェクトが作成されると、jest の config ファイルが自動で生成されます。

module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript',
  transform: {
    '^.+\\.vue$': 'vue-jest'
  }
}

また、下記のテストライブラリーがインストールされています。

ライブラリー名現在の最新バージョン
@types/jest^24.0.19
@vue/cli-plugin-unit-jest~4.5.0
@vue/test-utils^2.0.0-0
vue-jest^5.0.0-0
テスト関連ライブラリー一覧

サンプルコンポーネントの作成

Vue3 のアプリケーションの場合、複数のコンポーネントで構成され、
それぞれのコンポーネントが連動しながら動作します。

単体テストのコードはコンポーネントごとに書いていきます。
今回は Composition API を用いてコンポーネントの定義を行っていきます。

<template>
  <div>
    <div class="message">{{ uppercasedMessage }}</div>
    <div class="count">
      Count: {{ state.count }}
    </div>
    <button @click="increment">+</button>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive, computed } from 'vue'

export default defineComponent({
  props: {
    message: {
      type: String
    }
  },

  setup(props) {
    const state = reactive({
      count: 0
    })

    const increment = () => {
      state.count += 1
    }

    return {
      state,
      increment,
      uppercasedMessage: computed(() => props.message?.toUpperCase())
    }
  },
})
</script>

上記は以下の機能を持ったサンプルコンポーネントです。

  • props でテキストを受け取り、大文字に変換して表示
  • ボタンをクリックするとカウントが +1 される

このコンポーネントを使ってテストコードを作成していきます。

テストコードの作成

先ほど作成したコンポーネントでテストする項目は 2点あります。

  • props で受け取ったテキストは正しい処理を通して表示されるか
  • ボタンクリックで state.count は 1ずつ増加するか

実際に作成したコードがこちらです。

import { shallowMount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'

describe('Counter.vue', () => {

  // propsでの文字列表示
  it('renders a uppercase message', () => {
    const message = 'hoge'
    const wrapper = shallowMount(Counter, {
      props: { message }
    })
    // 表示箇所の文字列のチェック
    expect(wrapper.find(".message").text()).toBe('HOGE')
  })

  // ボタンクリックでのインクリメント
  it('increments a count when button is clicked', async () => {
    const message = 'hoge'
    const wrapper = shallowMount(Counter, {
      props: { message }
    })

    // クリックイベント発火
    await wrapper.find('button').trigger('click')

    // カウント表示箇所のチェック
    expect(wrapper.find('.count').text()).toBe('Count: 1')
  })

})

テストが通っている場合はこのような表示になると思います!

# テスト実行
$ yarn test:unit
PASS  tests/unit/Counter.spec.ts
  Counter.vue
    ✓ renders a uppercase message (76ms)
    ✓ increments a count when button is clicked (31ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.33s
Ran all test suites.
✨  Done in 7.61s.

失敗している場合は下記のように表示されます。 ExpectedReceived の出力の差分、エラー箇所がわかりやすく表示されるため、どこが原因となっているか素早く確認することができます。

FAIL  tests/unit/Counter.spec.ts (6.304s)
  Counter.vue
    ✕ renders a uppercase message (58ms)
    ✓ increments a count when button is clicked (13ms)

  ● Counter.vue › renders a uppercase message

    expect(received).toBe(expected) // Object.is equality

    Expected: "HOGE"
    Received: "Count: 0"

      10 |     })
      11 |     // 表示箇所の文字列のチェック
    > 12 |     expect(wrapper.find(".count").text()).toBe('HOGE')
         |                                           ^
      13 |   })
      14 |
      15 |   // ボタンクリックでのインクリメント

      at Object.<anonymous> (tests/unit/Counter.spec.ts:12:43)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        7.511s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Composition API によるテストのしやすさ

Composition API により、state などの変更を行う関数を別ファイルで管理できるようになりました。
これにより、メソッドなどのロジックのみを独立させてテストすることが容易になります。

ロジックのみが記述されたファイルを下記のように作成します。

~/src/composition/counter.ts

import { reactive, computed } from '@vue/composition-api';

export default (defaultCount = 0) => {
  const state = reactive({
    defaultCount
  })
  const count = computed(() => state.defaultCount)
  const increment = () => {
    state.defaultCount += 1
  }
  const decrement = () => {
    state.defaultCount -= 1
  }
  return {
    count,
    increment,
    decrement
  }
}

こちらは受け取った数字の上昇・減少を行う関数です。
先程のようにコンポーネント内でロジックを定義することもできますが、
このように分離させることでテスト時に簡潔に記述することができるのでおすすめです。


上記のロジックの場合、テストは下記のようになります。

~tests/unit/composition/useCount.spec.ts

import VueCompositionApi from '@vue/composition-api';
import { createLocalVue } from '@vue/test-utils';
import counter from '@/composition/counter';

const localVue = createLocalVue();
localVue.use(VueCompositionApi)

describe("counter", ()=> {
   test("increment", ()=> {
      const {count, increment} = counter(10)
      expect(count.value).toEqual(10)
      increment()
      expect(count.value).toEqual(11)
   })
   test("decrement", ()=> {
      const {count, decrement} = counter(10)
      expect(count.value).toEqual(10)
      decrement()
      expect(count.value).toEqual(9)
   })
})

テストコード内で localvue を定義しておりますが、こちらはテズとコード内で Composition API を使うために必須の設定です。

コンポーネント内のロジックのみの分離によりロジックのテストは容易になりました。
view 側のコンポーネントテストなどについては Storybook を用いることをおすすめします。

まとめ

今回は Vue3 で保守性の高いテストコードの作成方法について紹介しました。

Composition API でコンポーネントを定義しましたが、テストコードの作成においては Vue2 とあまり変わらないと思います。

Vue3 の Composition API を用いることでよりコンポーネントとロジックを疎結合な状態にでき、ロジックのみのテストも可能になりました。

Vue3 でのフロントエンドの開発のご相談は、ぜひお気軽にお問い合わせください。