# イベントをシミュレーションする

Vueコンポーネントの中でよくやることの1つはユーザーが発生したイベントをハンドルすることです。vue-test-utilsとJestでイベントのテストを書きやすくします。triggerとJestのモック関数を使ってコンポーネントのテストを書いてみましょう。

このガイドのテストのソースコードはこちらです。

# コンポーネントを作成する

<input><button>がある簡単な<FormSubmitter>コンポーネントを作ります。ボタンをクリックすると、何かが起こります。最初の例にボタンをクリックすると成功メッセージを表示します。次の例にボタンをクリックするとデータを外部サービスに送信します。

<FormSubmitter>を作って、テンプレートにこう書きます:

<template>
  <div>
    <form @submit.prevent="handleSubmit">
      <input v-model="username" data-username>
      <input type="submit">
    </form>

    <div 
      class="message" 
      v-if="submitted"
    >
      {{ username }}さん、お問い合わせ、ありがとうございます。
    </div>
  </div>
</template>

ユーザーはフォームを送信すると、メッセージを表示させます。フォームを非同期に送信するので、@submit.preventで送信します。そうしないと、デフォルトアクションが走ります。フォームを送信するとデフォルトアクションはページを更新します。

フォームを送信するロジックを追加します:

<script>
  export default {
    name: "FormSubmitter",

    data() {
      return {
        username: '',
        submitted: false
      }
    },

    methods: {
      handleSubmit() {
        this.submitted = true
      }
    }
  }
</script>

簡単です。送信するとsubmittedtrueにするだけです。

# テストを書く

テストをこう書きます:

import { shallowMount } from "@vue/test-utils"
import FormSubmitter from "@/components/FormSubmitter.vue"

describe("FormSubmitter", () => {
  it("フォームを更新するとお知らせを表示", () => {
    const wrapper = shallowMount(FormSubmitter)

    wrapper.find("[data-username]").setValue("alice")
    wrapper.find("form").trigger("submit.prevent")

    expect(wrapper.find(".message").text())
      .toBe("aliceさん、お問い合わせ、ありがとうございます。")
  })
})

テストがわかりやすいです。コンポーネントをマウントして、usernamesetValueで入力して、そしてvue-test-utilstriggerを使って送信することシミュレーションします。triggerをカスタムイベントにも使えるのでsubmit.preventmyEvent.doSomethingでも問題ないです。

このテストはユニットテストの3つの改行で分けました:

  1. arrange (初期設定) - テストの準備。この場合、コンポーネントをレンダーします
  2. act (実行) - システムを実行します。
  3. assert (検証)- 期待と検証を比べます。

ステップずつテストを分けるのが好きです。読みやすくなると思います。

yarn test:unitで実行すると、パスするはずです。

triggerの使い方は簡単です。ただイベントを発生させたい要素をfindで検証して、イベント名をtriggerに渡して呼び出します。

# 実例

アプリにフォームがよくあります。フォームのデータをエンドポイントに送信します。handleSubmitの実装を更新して、axiosというよく使われるHTTPクライエントで送信してみます。そしてそのコードのテストを書きます。

axiosVue.prototype.$httpにエイリアスすることもよくあります。詳しくはこちら。こうすると、this.$http.getを呼び出すだけでデータをエンドポイントに送信できます。

エイリアスして、this.$httpでフォームを送信する実装はこうです。

handleSubmitAsync() {
  return this.$http.get("/api/v1/register", { username: this.username })
    .then(() => {
      // メッセージを表示するなど
    })
    .catch(() => {
      // エラーをハンドル
    })
}

this.$httpをモックしたら、上のコードを簡単にテストできます。モックするにはmocksマウンティングオプションを使えます。mocksついて詳しくはこちらhttp.getのモック実装はこうです。

let url = ''
let data = ''

const mockHttp = {
  get: (_url, _data) => {
    return new Promise((resolve, reject) => {
      url = _url
      data = _data
      resolve()
    })
  }
}

いくつかの面白い点があります。

  • $http.getに渡すurldataを保存するためにurldata変数を作ります。そうすると、handleSubmitAsyncを呼び出すとただしいエンドポイントと正しいペイロードで動くか検証できます。- urldataをアサインしてから、Promiseをすぐにresolve(解決)します。これは正解となったレスポンスのシミュレーションです。

テストを書く前に、handleSubmitAsyncを更新します:

methods: {
  handleSubmitAsync() {
    return this.$http.get("/api/v1/register", { username: this.username })
      .then(() => {
        this.submitted = true
      })
      .catch((e) => {
        throw Error("Something went wrong", e)
      })
  }
}

そして<template>を更新して、新しいhandleSubmitAsyncを使います:

<template>
  <div>
    <form @submit.prevent="handleSubmitAsync">
      <input v-model="username" data-username>
      <input type="submit">
    </form>

  <!-- ... -->
  </div>
</template>

テスを書きましょう。

# AJAXコールをモックする

上に書いてあるモック関数をテストの最初のdescribeブロックの上に追加します。

let url = ''
let data = ''

const mockHttp = {
  get: (_url, _data) => {
    return new Promise((resolve, reject) => {
      url = _url
      data = _data
      resolve()
    })
  }
}

テストを書きましょう。mockHttpmocksに渡して、$httpの代わりに使います。

it("フォームを更新するとお知らせを表示", () => {
  const wrapper = shallowMount(FormSubmitter, {
    mocks: {
      $http: mockHttp
    }
  })

  wrapper.find("[data-username]").setValue("alice")
  wrapper.find("form").trigger("submit.prevent")

  expect(wrapper.find(".message").text())
    .toBe("aliceさん、お問い合わせ、ありがとうございます。")
})

こうすると、Vue.prototype.$httpの本当のAJAXライブラリーを使う代わりに、モックを使います。これがいいことです。テスト環境を簡単に扱います。

yarn test:unitを実行すると、テストが失敗します。

FAIL  tests/unit/FormSubmitter.spec.js
  ● FormSubmitter › フォームを更新するとお知らせを表示

    [vue-test-utils]: find did not return .message, cannot call text() on empty Wrapper

問題は、mockHttpが返却するPromiseresolveする前にテストの実行が終わりました。asyncをつけるとテストは同期に実行させます。

it("フォームを更新するとお知らせを表示", async () => {
  // ...
})

Promiseをすぐにresolveさせるライブラリーも必要です。よく使うのがflush-promisesです。yarn add flush-promisesでインストールできます。そしてテストを更新します。

import flushPromises from "flush-promises"
// ... 

it("フォームを更新するとお知らせを表示", async () => {
  const wrapper = shallowMount(FormSubmitter, {
    mocks: {
      $http: mockHttp
    }
  })

  wrapper.find("[data-username]").setValue("alice")
  wrapper.find("form").trigger("submit.prevent")

  await flushPromises()

  expect(wrapper.find(".message").text())
    .toBe("aliceさん、お問い合わせ、ありがとうございます。")
})

これでテストがパスします。flush-promiseのソースコードが10行だけなので、読んでみて、理解することがおすすめです。

エンドポイントとペイロードが正しいかを検証することもできます。2つの検証をテストに追加します。

// ...
expect(url).toBe("/api/v1/register")
expect(data).toEqual({ username: "alice" })

テストはパスします。

# まとめ

このガイドで学んだことは:

  • triggerを使ってイベントを発火させること
  • setValuev-modelを使う<input>の値を設定する
  • ユニットテストを3つのステップに分けること。(初期設定、実行、検証)
  • Vue.prototypeのメソッドをモックする
  • flush-promisesを使ってPromiseをすぐにresolveさせる

このガイドのテストのソースコードはこちらです。