💯

Vue 3 テストのチュートリアル

2022/04/25に公開

Vueでテストを書く

Vueでのテストの書き方を知るために、チュートリアルを作ってみました。

セットアップ

成果物のリポジトリはこちら: https://github.com/naga3/vue-test-example

# yarn global add @vue/cli  # Vue CLIが入っていない場合
vue create vue-test-example
cd vue-test-example
yarn serve

以下のオプションで作成しました。Unit Testing だけは有効にして、あとはご自由にどうぞ。

Please pick a preset: Manually select features
Check the features needed for your project: Babel, TS, Router, CSS Pre-processors, 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
)? Yes
Use history mode for router? (Requires proper server setup for index fallback in production) Yes
Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SC
SS (with dart-sass)
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

作られたテストの内容を眺めてみる

Unit Testing を有効にするとサンプルのテストがひとつ作られますので、それを眺めてみます。

テスト対象のコンポーネント

https://github.com/naga3/vue-test-example/blob/main/src/components/HelloWorld.vue

msg というプロパティを受け取って、それを表示するだけの単純なコンポーネントです。

テストファイル

https://github.com/naga3/vue-test-example/blob/main/tests/unit/example.spec.ts

describe はテストをグルーピングするもので、テスト中に概要 (HelloWorld.vue) が表示されるので、どのテストで落ちたかを知るためにも有用です。describe は入れ子にすることができます。

it の中に実際のテストを書いて行きます。ittest の別名で、どちらで書いても構いません。

コンポーネントをテストするためにはまずマウントする必要があり、そのために mountshallowMount のどちらかを使います。mount は子コンポーネントも含めた完全な状態でマウントされますが、shallowMount は子コンポーネントはスタブになります。くわしくは こちら を参照してください。
props にはコンポーネントに渡すプロパティを記述します。

mount または shallowMount はコンポーネントをマウントしたインスタンスの薄いラッパー VueWrapper を返します。VueWrapper はテストで有用なメソッドがいくつか実装されていて、text はコンポーネントのテキストを返します。

テスト実行

yarn test:unit

$ yarn test:unit
yarn run v1.22.18
$ vue-cli-service test:unit
 PASS  tests/unit/example.spec.ts
  HelloWorld.vue
    ✓ renders props.msg when passed (17 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.159 s, estimated 2 s
Ran all test suites.
Done in 1.70s.

describeit に書いた説明が表示されているのが確認できます。

簡単なコンポーネントのテストを書いてみる

では実際にテストを書いてみます。最初は簡単なフォームコンポーネントを作成します。

コンポーネント作成

このようなコンポーネントを作成します。

MyForm1

コメントを書いてボタンを押すと、メッセージが表示されます。

コンポーネントのソースはこちら。

https://github.com/naga3/vue-test-example/blob/main/src/components/MyForm1.vue

  • comment は入力フィールドに入力した文字列です。
  • receiver は受信者で、"A", "B", null のいずれかが入ります。

data-testid 属性はテストの要素を特定するために設定しています。IDやclassから特定することもできるのですが、そうするとデザインの変更にテストが引っ張られてしまいますので、data-* 属性を使ったほうがベターです。詳しくはこちらの記事を参照してください。 https://qiita.com/akameco/items/519f7e4d5442b2a9d2da

テストを実装する

このフォームのテストではどのようなテスト項目が必要かを考えてみます。

  • 最初は何もメッセージが表示されていない
  • コメントを入力せずに「Aさんに送る」ボタンを押すと「コメントがありません。」と表示される
  • コメントを入力せずに「Bさんに送る」ボタンを押すと「コメントがありません。」と表示される
  • コメントを入力して「Aさんに送る」ボタンを押すと「Aさんに『コメント』を送りました。」と表示される
  • コメントを入力して「Bさんに送る」ボタンを押すと「Bさんに『コメント』を送りました。」と表示される
  • メッセージが表示されているときに違うコメントを入力すると、メッセージが消える

以上の項目を実装したテストがこちら。

https://github.com/naga3/vue-test-example/blob/main/tests/unit/MyForm1.spec.ts

まずコンポーネントをマウントし、VueWrapper.find メソッドで data-testid 属性の付与された要素を取得します。

  const elemComment = wrapper.find('[data-testid="input-comment"]')
  const elemSendA = wrapper.find('[data-testid="button-send-a"]')
  const elemSendB = wrapper.find('[data-testid="button-send-b"]')
  const elemMessage = () => wrapper.find('[data-testid="message"]')

elemComment が入力フィールド、elemSendA が「Aさんに送る」ボタン、elemSendB が「Bさんに送る」ボタン、elemMessage がメッセージ表示エリアの要素となります。elemMessage だけが関数になっているのは、v-if で要素が消える可能性があるためです。

  await elemSendA.trigger('click')

テスト内でボタンを押下するためには VueWrapper.trigger メソッドで click イベントを送ります。

  await elemComment.setValue('こんにちは')

テスト内で入力フィールドに文字列を入力するためには VueWrapper.setValue メソッドを使います。

setValue や trigger を使うときは、VueがDOMを更新するまで待つために、awaitする必要があります。詳しくはこちら

props と emit のテスト

ほとんどのコンポーネントでは、親コンポーネントから送られるプロパティや、親コンポーネントにイベントを送る emit があると思いますので、そのテストを書きます。

コンポーネント作成

このようなコンポーネントを作成します。

MyForm2

  • 受信者は props で受け取ります。
  • コメントを書くとリアルタイムでメッセージに反映されます。
  • 送信ボタンを押すとコメントが emit されます。

コンポーネントのソースはこちら。

https://github.com/naga3/vue-test-example/blob/main/src/components/MyForm2.vue

  • receiver は受信者を受け取るプロパティです。
  • comment は入力フィールドに入力した文字列です。
  • typingMessage 入力中のメッセージで、computed によってリアルタイムに反映されます。
  • sendComment は送信ボタンを押したときに呼び出される関数で、sendComment イベントを emit します。

テストを実装する

このフォームのテストではどのようなテスト項目が必要かを考えてみます。

  • props で渡された受信者が送信ボタンに反映されていること
  • 最初はメッセージが空であること
  • コメント入力中は、入力中のメッセージが表示されること
  • 送信ボタンを押すとコメントが emit されること

以上の項目を実装したテストがこちらです。

https://github.com/naga3/vue-test-example/blob/main/tests/unit/MyForm2.spec.ts

  const wrapper = shallowMount(MyForm2, {
    props: { receiver: '鈴木' }
  })

コンポーネントをマウントするときにオプションで props を渡します。

  const sendEvent = wrapper.emitted('sendComment')

sendComment イベントの emit 結果を取得しています。
VueWrapper.emitted で emit された情報を取得できます。

  expect(sendEvent).toHaveLength(1)

emit は複数回呼ばれる可能性があるので、emitted の戻り値は配列となります。
こちらで1回しか呼ばれていないことをテストしています。

  expect(sendEvent?.[0]).toEqual(['こんばんは'])

emit したコメントの内容をテストしています。

emitted は undefined を戻す可能性があるので、型エラーを防ぐためにオプショナルチェーンでアクセスしています。

子コンポーネントのスタブ化

例えば、親コンポーネントが子コンポーネントを呼び出していて、子コンポーネントで外部へのAPIアクセスなどがある場合、子コンポーネントをスタブ化しておくと単体テストがやりやすくなる場合があります。

コンポーネント作成

このようなコンポーネントを作成します。

MyForm3

親コンポーネントのソースはこちら。

https://github.com/naga3/vue-test-example/blob/main/src/components/MyForm3.vue

子コンポーネントのソースはこちら。

https://github.com/naga3/vue-test-example/blob/main/src/components/MyForm3Child.vue

  • 子コンポーネントに props で入力フィールドの値を渡します。
  • 子コンポーネントで渡された値を2倍し、メッセージを表示します。

テストを実装する

  • 親コンポーネントと子コンポーネントの両方をマウントする
  • 親コンポーネントのみマウントし、子コンポーネントはスタブとする

この2種類のテストを実装してみます。

https://github.com/naga3/vue-test-example/blob/main/tests/unit/MyForm3.spec.ts

  const MyForm3ChildStub = {
    props: {
      number: {
        type: Number,
        required: true
      }
    },
    template: '<div>{{ number }}を2倍すると{{ number * 2 }}です</div>'
  }

スタブコンポーネントです。元の MyForm3Child コンポーネントとほぼ同じ機能なのであまりスタブとは言えないかも知れませんが、このようにコンポーネントをまるっと入れ替えることもできます。

  const wrapper = mount(MyForm3, {
    global: {
      stubs: {
        MyForm3Child: MyForm3ChildStub
      }
    }
  })

mount のオプションで指定することでスタブ化できます。

Jest の mock 機能を使ってみる

Vueファイルでない場合は、Jest の mock 機能を使うと便利です。

コンポーネント作成

このようなコンポーネントを作成します。

MyForm4

コンポーネントのソースはこちら。

https://github.com/naga3/vue-test-example/blob/main/src/components/MyForm4.vue

  • 入力フィールドの値を double 関数を呼び出して2倍にし、メッセージを表示します。

double 関数のソースはこちら。

https://github.com/naga3/vue-test-example/blob/main/src/components/double.ts

テストを実装する

double 関数をモックにしたテストを実装します。

https://github.com/naga3/vue-test-example/blob/main/tests/unit/MyForm4.spec.ts

jest.mock('@/components/double')

double.ts をモック化しています。

afterEach(() => {
  jest.resetAllMocks()
})

モックの呼び出し回数や戻り値のセットはテスト間でリセットされないので、resetAllMocks で毎回リセットしています。

  (double as jest.Mock).mockReturnValue(6)

モック化した double 関数の戻り値を mockReturnValue によって 6 にセットしています。その際、double 関数には mockReturnValue プロパティは生えていないので、型エラーが出ないように型アサーションしています。

  expect(double).toHaveBeenCalledTimes(2)

toHaveBeenCalledTimes で double 関数の呼ばれた回数をテストしています。マウント時と setValue 時で2回呼ばれます。

  expect(double).toHaveBeenLastCalledWith(3)

toHaveBeenLastCalledWith で最後に double 関数が呼ばれたときの引数をテストしています。

参考文献

Discussion