「後悔しないためのVueコンポーネント設計」の感想・備忘録2

スポンサーリンク
「後悔しないためのVueコンポーネント設計」の感想・備忘録1の続き

なにをテストするか

テストの対象

筆者は以下をコンポーネントのテスト項目としている。

  • templateでv-on, v-bind:class, v-bind:attrsが正しく動作するか
  • propsが正しく受け取れるか
  • methodsが正しく動作するか
  • $emitでイベントが正しく発火するか
  • slotが正しく動作するか

テストを書く

vue-test-utils

  • テスト実行環境の準備
    npx vue create xxxでのプロジェクト作成時にUnit Testing⇒Jestを有効にし、npm run test:unitで実行
  • vue-test-utilsではコンポーネントをmount()かshallowMount()でマウントする。
    • mount: 子コンポーネントに実際のコンポーネントを使う。
    • shallowMount: 子コンポーネントにダミーコンポーネントを使う。
  • 基本的にはshallowMountを使う。 子コンポーネントの動作まで検証が必要な場合のみmountを使う。
    const wrapper = shallowMount(Hoge, { propsData })
    • shallowMountはWrapperオブジェクトを返す。
    • Wrapperは以下のメソッドを持っている。
      • props(), setProps(): propsのゲッターとセッター。
      • emitted(): $emit()されたイベントと渡された引数を返す。
      • find(), findAll(): DOMノードを取得する。
      • findComonent: コンポーネントを取得。

basicのテスト: SiteTitle

<template>
  <div class="SiteTitle">
    {{title}}
  </div>
</template>

<script>
export default {
  name: "SiteTitle",
  props: {
    title: String,
  },
}
</script>
import { shallowMount } from "@vue/test-utils"
import SiteTitle from "@/basics/SiteTitle.vue"
describe("SiteTitle.vue", () => {
  const propsData = {
    title: "test title",
  }
  it("props", () => {
    const wrapper = shallowMount(SiteTitle, { propsData })
    expect(wrapper.props()).toEqual(propsData)
  })
  describe("template", () => {
    it("snapshot", () => {
      const wrapper = shallowMount(SiteTitle, { propsData })
      expect(wrapper.vm.$el).toMatchSnapshot()
    })
  })
})

componentのテスト: MenuItem

<template>
  <div class="MenuItem">
    <span @click="clickMenuItem" >{{ label }}</span>
  </div>
</template>

<script>
export default {
  name: "MenuItem",
  props: {
    label: String,
    name: String,
  },
  methods: {
    clickMenuItem() {
      this.$emit("clickMenuItem", {name: this.name})
    },
  },
}
</script>
import { shallowMount } from "@vue/test-utils"
import MenuItem from "@/components/Menu/MenuItem.vue"
import menuItems from "../../_mockData/menuItems.json"
describe("MenuItem.vue", () => {
  const propsData = menuItems[0]
  it("props", () => {
    const wrapper = shallowMount(MenuItem, { propsData })
    expect(wrapper.props()).toEqual(propsData)
  })
  describe("methods", () => {
    it("clickMenuItem", () => {
      const wrapper = shallowMount(MenuItem, { propsData })
      wrapper.vm.clickMenuItem()
      expect(wrapper.emitted("clickMenuItem")).toBeTruthy()
      expect(wrapper.emitted("clickMenuItem")[0][0]).toEqual({
        name: propsData.name,
      })
    })
  })
  describe("template", () => {
    it("snapshot", () => {
      const wrapper = shallowMount(MenuItem, { propsData })
      expect(wrapper.vm.$el).toMatchSnapshot()
    })
  })
})

componentのテスト: MainMenu

  • MainMenuは子コンポーネントとしてMenuItemを使用しているため、shallowMountではなくmountを使用する。
<template>
  <div class="Menu">
    <MenuItem class="Menu_Item" v-for="item in items" :key="item.label" v-bind="item" @clickMenuItem="onClickMenuItem" />
  </div>
</template>

<script>
import MenuItem from "./Menu/MenuItem.vue"
export default {
  name: "MainMenu",
  components: {
    MenuItem,
  },
  props: {
    items: Array,
  },
  methods: {
    onClickMenuItem({name}) {
      this.$emit("clickMenuItem", {name})
    },
  },
}
</script>
import { mount } from "@vue/test-utils"
import Menu from "@/components/MainMenu.vue"
import MenuItem from "@/components/Menu/MenuItem.vue"
import menuItems from "../_mockData/menuItems.json"
describe("MainMenu.vue", () => {
  const propsData = {
    items: menuItems,
  }
  it("props", () => {
    const wrapper = shallowMount(Menu, { propsData })
    expect(wrapper.props()).toEqual(propsData)
  })
  describe("methods", () => {
    it("clickMenuItem", () => {
      const wrapper = mount(Menu, { propsData })
      wrapper.vm.onClickMenuItem(menuItems[0])
      expect(wrapper.emitted("clickMenuItem")).toBeTruthy()
      expect(wrapper.emitted("clickMenuItem")[0][0]).toEqual({
        name: menuItems[0].name,
      })
    })
  })
  describe("template", () => {
    it("snapshot", () => {
      const wrapper = mount(Menu, { propsData })
      expect(wrapper.vm.$el).toMatchSnapshot()
    })
  })
  // mountによる結合テスト
  it("@clickMenuItem=onClickMenuItem", () => {
    const wrapper = mount(Menu, { propsData })
    const menuItem = wrapper.findComponent(MenuItem)
    menuItem.vm.clickMenuItem()
    expect(menuItem.emitted("clickMenuItem")).toBeTruthy()
    expect(menuItem.emitted("clickMenuItem")[0][0]).toEqual({
      name: menuItems[0].name,
    })
  })
})

containerのテスト: GlobalHeader

  • VuexをテストするためにlocalVueを使う。
<template>
  <header class="GlobalHeader">
    <SiteLogo class="GlobalHeader_Logo"/>
    <span class="GlobalHeader_SiteTitle" @click="navigateRoot" >
      <SiteTitle :title="siteTitle"/>
    </span>
    <MainMenu class="GlobalHeader_Menu" :items="menuItems" @clickMenuItem="onClickMenuItem" />
  </header>
</template>

<script>
import {mapState} from "vuex"
import SiteLogo from "../basics/SiteLogo.vue"
import SiteTitle from "../basics/SiteTitle.vue"
import MainMenu from "../components/MainMenu.vue"

export default {
  name: "GlobalHeader",
  components: {
    SiteLogo,
    SiteTitle,
    MainMenu,
  },
  computed: {
    ...mapState(["siteTitle", "menuItems"]),
  },
  methods: {
    onClickMenuItem({name}) {
      this.$emit("navigate", {name})
    },
    navigateRoot() {
      this.$emit("navigate", {name: "root"})
    },
  },
}
</script>
import { shallowMount, mount, createLocalVue } from "@vue/test-utils"
import GlobalHeader from "@/containers/GlobalHeader.vue"
import Menu from "@/components/MainMenu.vue"
import Vuex from "vuex"
import menuItems from "../_mockData/menuItems.json"
const localVue = createLocalVue()
localVue.use(Vuex)
describe("GlobalHeader.vue", () => {
  let store

  // 毎テストケース実行前にstoreを初期化する
  beforeAll(() => {
    store = new Vuex.Store({
      state: {
        siteTitle: "test site title",
        menuItems,
      },
    })
  })

  describe("methods", () => {
    it("clickMenuItem", () => {
      // Vueインスタンス作成時にstoreとlocalVueを渡す
      const wrapper = shallowMount(GlobalHeader, { store, localVue })
      wrapper.vm.onClickMenuItem(menuItems[0])
      expect(wrapper.emitted("navigate")).toBeTruthy()
      expect(wrapper.emitted("navigate")[0][0]).toEqual({
        name: menuItems[0].name,
      })
    })
    it("navigateRoot", () => {
      const wrapper = shallowMount(GlobalHeader, { store, localVue })
      wrapper.vm.navigateRoot()
      expect(wrapper.emitted("navigate")).toBeTruthy()
      expect(wrapper.emitted("navigate")[0][0]).toEqual({
        name: "root",
      })
    })
  })

  describe("template", () => {
    it("@click=navigateRoot", () => {
      const wrapper = shallowMount(GlobalHeader, { store, localVue })
      wrapper.find(".GlobalHeader_SiteTitle").trigger("click")
    })
    it("@clickMenuItem=onClickMenuItem", () => {
      const wrapper = shallowMount(GlobalHeader, { store, localVue })
      const menu = wrapper.findComponent(Menu)
      menu.vm.$emit("clickMenuItem", {})
    })
    it("snapshot", () => {
      const wrapper = mount(GlobalHeader, { store, localVue })
      expect(wrapper.vm.$el).toMatchSnapshot()
    })
  })
})

pageのテスト: GlobalHeader

  • Vue RouterをテストするためにlocalVueを使う。
<template>
  <div class="Root">
    <GlobalHeader @navigate="onNavigate"/>
    <SiteLogo class="Root_Logo"/>
  </div>
</template>

<script>
import GlobalHeader from "../containers/GlobalHeader.vue"
import SiteLogo from "../basics/SiteLogo.vue"
export default {
  name: "RootPage",
  components: {
    GlobalHeader,
    SiteLogo,
  },
  methods: {
    onNavigate({ name }) {
      this.$router.push({ name })
    },
  },
}
</script>
import { createLocalVue, shallowMount } from "@vue/test-utils"
import Vuex from "vuex"
import menuItems from "../_mockData/menuItems.json"
import Root from "@/pages/RootPage.vue"
import GlobalHeader from "@/containers/GlobalHeader.vue"
const localVue = createLocalVue()
localVue.use(Vuex)
describe("RootPage.vue", () => {
  let store
  let $router
  beforeAll(() => {
    store = new Vuex.Store({
      state: {
        siteTitle: "test site title",
        menuItems,
      },
    })
    // $routerのモックを作成
    $router = {
      push: jest.fn(), // $router.pushをモック関数にしておく
    }
  })
  describe("methods", () => {
    it("onNavigate", () => {
      // Vueインスタンス作成時に$routerプロパティを注入する
      const wrapper = shallowMount(Root, { store, localVue, mocks: { $router } })
      wrapper.vm.onNavigate({ name: "root" })
      // mock化した$router.pushが呼ばれているか
      expect($router.push).toHaveBeenCalledWith({
        name: "root",
      })
    })
  })
  describe("template", () => {
    it("snapshot", () => {
      const wrapper = shallowMount(Root, { store, localVue, mocks: { $router } })
      expect(wrapper.vm.$el).toMatchSnapshot()
    })
  })
})

コメント