Help us understand the problem. What is going on with this article?

Vue.jsのテストでwrap越しに子コンポーネントのevent up($emit)をエミュレートする方法

概要

Vue.js(Vue CLI v4系)で、wrap = shallowMount() 越しに次のことをテストする方法について記載する。

  • 子コンポーネント(Component)からevent upされた状態を検証する方法
  • 対象のVueコンポーネントからevent upしたことを検証する方法

内容としては、公式サイトのガイドやAPI仕様書に記載されている通りなのだが、私はそれだけでは「実際にどのようにするのか?」を読み取れなかったので、その観点でのメモが目的。

対象とする被テストのコンポーネント

例として、画像ファイルを登録する前の説明ページを使う。
「ファイルの取り扱い」ダイアログを表示して、
その後に「同意する」チェックボックスを押した場合にのみ、
「画像を登録」ボタンを押せる仕様、とする。

実際の画像登録は、親コンポーネントへemit()して処理するものとし、
被テストのコンポーネントでは取り扱わない。

Explanation.vue
<template>
<div>
    <div id="id_button_confirm_privacy" v-on:click="openModalOfPrivacy">
        <span>【登録された画像ファイルの取り扱いについて】</span>
    </div>
    <!-- コンポーネント ModalConfirmPrivacy -->
    <ModalConfirmPrivacy id="id_modaldialog_pivacy" v-on:close="closeModalOfPrivacy" v-if="isModalDialog">
        <h3 slot="header">登録ファイルの取り扱いについてダイアログ</h3>
        <span slot="body">
            ここに任意のテキストを入れる。<br>
        </span>
    </ModalConfirmPrivacy>
    <br>
    <div id="id_checkbox_agree_privacy" v-on:click="toggleAgree">
        <span id="id_agree2policy" v-show="isAgree">[レ]</span>
        <span id="id_not_agree2policy" v-show="!isAgree">[ _ ]</span>
        <span>「ファイルの取り扱い」に同意する</span>
    </div>
    <br>
    <div id="id_button_select_image" v-on:click="go2Next">
        【画像を登録】
    </div>
</div>
</template>

<script>
// javascriptファイルをココへ配置
import ModalConfirmPrivacy from './ModalConfirmPrivacy.vue';

export default {
    name : "Explanation",
    components : { ModalConfirmPrivacy },
    data : function () {
        return {
            isModalDialog: false,
            isHavingSeen : false,
            isAgree : false
        }
    },
    methods : {
        openModalOfPrivacy : function () {
            this.isModalDialog = true;
        },
        closeModalOfPrivacy : function () {
            this.isModalDialog = false;
            this.isHavingSeen = true;
        },
        toggleAgree : function () {
            this.isAgree = (this.isHavingSeen) ? !this.isAgree : false;
        },
        go2Next : function () {
            if(this.isAgree){
                this.$emit('go2SelectImage')
            }
        },
    }
}
</script>

<style scoped>
/* Cssファイルはここへ配置する。 */
#id_button_confirm_privacy {
    cursor: pointer;
    text-decoration: underline;
}
#id_button_select_image {
    cursor: pointer;
    text-decoration: underline;
}
</style>

以下の環境で動作検証

    "vue": "^2.6.11"

    "@vue/cli-plugin-babel": "~4.2.0",
    "@vue/cli-plugin-eslint": "~4.2.0",
    "@vue/cli-plugin-unit-mocha": "~4.2.0",
    "@vue/cli-service": "~4.2.0",
    "@vue/test-utils": "^1.0.0-beta.29",
    "babel-eslint": "^10.0.3",
    "chai": "^4.1.2",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.1.2",
    "vue-template-compiler": "^2.6.11"

※なお、vue create でインストールされる
@vue/test-utils@1.0.0-beta.31 だとtrigger() でDOMが更新されないように見える。
@vue/test-utils@1.0.0-beta.29 だと問題ない。
なので、vue create した後で、npm install @vue/test-utils@1.0.0-beta.29 --save-dev にて上書きしてダウングレードインストールして動作検証した。

※↑の理解、誤り。DOMの更新は、$nextTick()をもって待たねばならないのが仕様。
beta.29で動作したのは偶々だろう。
たとえば次のようにPromise(もしくはawiteショートハンド)してから先に進めばよい。
後で記事を修正する。

  // DOM変化(v-show, v-if)を伴った応答をテストするには、DOMの更新後を待つ必要がある。
  const waitVm = function (wrapper) {
    return new Promise((resolve)=>{
      wrapper.vm.$nextTick(()=>{
        resolve();
      })
    })
  };

子コンポーネントからのevent upを、スタブを用いてエミュレートする

Explanation.vueをshallowMount()でwrapper生成して、
コンポーネントModalConfirmPrivacyの close イベントに紐づけられた
closeModalOfPrivacy() が呼ばれた状態をエミュレートするには、
次にようにする。

  1. shallowMount() に stabs オプションを指定して、「closeイベントを$emitするノードを持ったスタブ」を設定する
  2. スタブ内のノードを trigger()して $emitを経由してcloseイベントを発火する

これにより、closeModalOfPrivacy() を直に呼び出すのではなく、
あくまでも close イベントに紐づけられたソレを呼び出した状態、を検証できる。

具体的なテストコードの例は以下となる。

Explanation.spec.js
import { shallowMount } from '@vue/test-utils';
import { expect } from 'chai';
import targetVue from '@/components/Explanation';

describe('Explanation.vue', () => {
  const factory = (propsData)=>{
    return shallowMount(targetVue,{
      stubs: {
        ...propsData
      }
    });
  };

  describe('画像ファイルの取り扱いへ同意のチェックボックス', ()=>{
    it('ダイアログModalConfirmPrivacyでcloseModalOfPrivacy()が呼ばれた後は、id_checkbox_agree_privacyをクリックしてid_agree2policyが表示(チェック)へ変化する', () => {
      const wrapper = factory({
        ModalConfirmPrivacy : '<div><span id="_stub_id_modal_close" @click="$emit(\'close\')">closeボタンだけを持ったモーダルダイアログへスタブ化</span></div>'
      });

      // チェックボックス(相当)の初期状態を検証
      expect(wrapper.find('#id_agree2policy').isVisible()).to.be.false;
      expect(wrapper.find('#id_not_agree2policy').isVisible()).to.be.true;

      // ダイアログを開いて閉じる。
      wrapper.find('#id_button_confirm_privacy').trigger('click');
      wrapper.find('#_stub_id_modal_close').trigger('click'); // ↑をclickしてからでないと、これは取れない。

      // チェックボックス(相当)をクリック
      wrapper.find('#id_checkbox_agree_privacy').trigger('click');

      // チェックボックス(相当)が変更されたことを検証
      expect(wrapper.find('#id_agree2policy').isVisible()).to.be.true;
      expect(wrapper.find('#id_not_agree2policy').isVisible()).to.be.false;
    });
  });
});

被テスト対象のVueコンポーネントで $emit() されたことを検証する方法

コンポーネント内で「this.$emit('発火するイベント名称') が呼ばれたか?」
を検証するには emitted() を用いる。

具体的なテストコードの例は以下となる。

Explanation.spec.js
import { shallowMount } from '@vue/test-utils';
import { expect } from 'chai';
import targetVue from '@/components/Explanation';

describe('Explanation.vue', () => {
  const factory = (propsData)=>{
    return shallowMount(targetVue,{
      stubs: {
        ...propsData
      }
    });
  };


  describe('画像ファイル登録ボタン', ()=>{
    it('id_agree2policyが表示(チェック済み)ならば、親コンポーネントのgo2SelectImage()が呼ばれること', () => {
      const wrapper = factory({
        ModalConfirmPrivacy : '<div><span id="_stub_id_modal_close" @click="$emit(\'close\')">closeボタンだけを持ったモーダルダイアログへスタブ化</span></div>'
      });

      // ダイアログを開いて閉じる。
      wrapper.find('#id_button_confirm_privacy').trigger('click');
      wrapper.find('#_stub_id_modal_close').trigger('click'); // ↑をclickしてからでないと、これは取れない。
      // チェックボックス(相当)をクリック
      wrapper.find('#id_checkbox_agree_privacy').trigger('click');

      // 画像登録ボタンをクリック
      wrapper.find('#id_button_select_image').trigger('click');

      // 親コンポーネントのgo2SelectImage()を呼び出したことを検証
      // ※引数がある場合は、キー(例:'go2SelectImage')のバリューとして配列で入ってくる。
      //   今回の事例では引数無しなので省略。
      expect(wrapper.emitted()).has.property('go2SelectImage');
    });
  });
});

以上ー。

参考サイト

hoshimado
趣味で日曜プログラミング。仕事はたぶんIT関連? この2016年春から、ふとJavaScript周りに興味が沸いたので、Webアプリベースで「自分が便利」ツール作成しつつ、その裏で「コピペで使えるコード」の共有と勉強を目的にQiitaに参戦。https://twitter.com/hoshimado7
http://fluorite.halfmoon.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした