ぽぴなび

知って感動した技術情報・生活情報や買ってよかったものの雑記です。

【Vue】Vue x TypeScript x Vuetify プロジェクトのテスト環境(Jest)を整える

Vue公式のやり方で、TypeScriptに対応した後Vuetifyを追加するとエラーが出て長時間詰まったので別のやり方を試すことに。。

手順

  1. vue create my-sample-projでプロジェクトを作成する(この際にTypeScript, Unit Testingを有効にしておく)
  2. vue add vuetifyでVuetifyを導入
  3. Vuetifyを使ったコンポーネントを作成
  4. Vuetifyコンポーネントをテストする際の設定
  5. テストコードを書いてみる

1. TypeScriptとUnit Testingを導入したプロジェクトの作成

まずはvue createを実行。
? Please pick a preset:ではManually select featuresを選択。

? Check the features needed for your project:では、Choose Vue version, Babel,TypeScript,Unit Testingにチェックを入れる。(ほかはお好みでOKと思われる)

他の設定は基本デフォルトで、? Pick a unit testing solution:ではJestを選択する。

vue create vue-test-sample-ts

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 2.x
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Pick a linter / formatter config: Prettier
? 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

確認

vue createが完了すると、tests/unitexample.spec.tsが作成されており、最初からTypeScriptに対応していることがわかる。
npm run test:unit でテストを実行すると、1つだけあるテストが成功するはず。

npm run test:unit

 PASS  tests/unit/example.spec.ts
  HelloWorld.vue
    ✓ renders props.msg when passed (20ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.271s
Ran all test suites.

2. プロジェクトにVuetifyを追加する

vue add vuetifyを実行するとHelloWorld.vueが書き換えられてしまうが(←example.spec.tsが動かなくなる)、実行後にHelloWorld.vueの変更を取り消しすればOK。

念の為、ここでもnpm run test:unitを実行しておく。

npm run test:unit

PASS  tests/unit/example.spec.ts
  HelloWorld.vue
    ✓ renders props.msg when passed (17ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.571s, estimated 3s
Ran all test suites.

3. Vuetifyコンポーネントを作成する

Vuetifyのサンプルに掲載してある、ボタンを押すと開閉するカードを使う。

<template>
  <v-card class="mx-auto ma-4" max-width="344">
    <v-img
      src="https://cdn.vuetifyjs.com/images/cards/sunshine.jpg"
      height="200px"
    ></v-img>

    <v-card-title> Top western road trips </v-card-title>

    <v-card-subtitle> 1,000 miles of wonder </v-card-subtitle>

    <v-card-actions>
      <v-btn color="orange lighten-2" text> Explore </v-btn>

      <v-spacer></v-spacer>

      <v-btn icon @click="show = !show">
        <v-icon>{{ show ? "mdi-chevron-up" : "mdi-chevron-down" }}</v-icon>
      </v-btn>
    </v-card-actions>

    <v-expand-transition>
      <div v-show="show">
        <v-divider></v-divider>

        <v-card-text>
          I'm a thing. But, like most politicians, he promised more than he
          could deliver. You won't have time for sleeping, soldier, not with all
          the bed making you'll be doing. Then we'll go with that data file!
          Hey, you add a one and two zeros to that or we walk! You're going to
          do his laundry? I've got to find a way to escape.
        </v-card-text>
      </div>
    </v-expand-transition>
  </v-card>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";

@Component
export default class CustomCard extends Vue {
  show = false;
}
</script>

See the Pen Vuetify Example Pen by popy1017 (@popy1017) on CodePen.

仕様としては、左下の「EXPLORE」ボタンを押しても何も起きないが、右下のアイコンボタンを押すと詳細情報が表示されるというもの。

4. Vuetifyコンポーネントをテストする際の設定

この設定を行わないとv-cardなどのVuetify固有のタグが認識されずエラーや警告が出てしまう。

[Vue warn]: Unknown custom element: <v-card> - did you register the component correctly? For recursive components, make sure to provide the "name" option.

tests/配下に以下のsetup.tsを用意する。

import Vue from "vue";
import Vuetify from "vuetify";

Vue.use(Vuetify);

さらに、jest.config.jsに以下を追加する。

module.exports = {
  ~~ 
  setupFiles: ["./tests/setup.ts"],
};

テストファイルが少ない場合などは、setup.tsの内容をspec.tsファイルに直書きでもOK。

参考記事

ユニットテスト — Vuetify

5. テストコードを書く

今回は以下の観点でテストコードを作成する。

  • 画面描画時に詳細(v-card-text)は非表示になっている
  • 右下のアイコンボタンをクリックすると、詳細が表示される

注意点

あまりよくわかっていないが、 テスト対象のコンポーネントmountする際は以下のようにする必要がある。

import { createLocalVue, mount, shallowMount } from "@vue/test-utils";
import CustomCard from "@/components/CustomCard.vue";
import Vuetify from "vuetify";

describe("CustomCard.vue", () => {
  const localVue = createLocalVue();
  let vuetify: Vuetify;

  beforeEach(() => {
    vuetify = new Vuetify();
  });

  it("should hide details when rendered", async () => {
    // マウントするときに localVue, vuetify を一緒に渡す。
    const wrapper = mount(CustomCard, { localVue, vuetify });  

shallowMountの際はなくても問題ないと思われる。

ユニットテスト — Vuetify

作成したテストコード

import { createLocalVue, mount, shallowMount } from "@vue/test-utils";
import CustomCard from "@/components/CustomCard.vue";
import Vuetify from "vuetify";

describe("CustomCard.vue", () => {
  const localVue = createLocalVue();
  let vuetify: Vuetify;

  beforeEach(() => {
    vuetify = new Vuetify();
  });

  it("should have data named 'show' with default false when rendered", () => {
    const wrapper = shallowMount(CustomCard);

    expect(wrapper.vm.$data.show).toBe(false);
  });

  it("should hide details when rendered", async () => {
    const wrapper = mount(CustomCard, { localVue, vuetify });
    const details = wrapper.find(".v-card__text");

    expect(details.isVisible()).toBe(false);
  });

  it("should show details when icon button clicked", async () => {
    const wrapper = mount(CustomCard, { localVue, vuetify });

    const details = wrapper.find(".v-card__text");
    const button = wrapper.find(".v-btn--icon");

    await button.trigger("click");

    expect(details.isVisible()).toBe(true);
  });
});

表示・非表示の切り替え変数の値をチェックする

wrapper.vm.$data.{変数名}で値をチェックできる。

  it("should have data named 'show' with default false when rendered", () => {
    const wrapper = shallowMount(CustomCard);

    expect(wrapper.vm.$data.show).toBe(false);
  });

v-card-textが表示されているかどうかをチェックする

要素.isVisible()で確認できる。

style が display: none か visibility: hidden の親要素がある場合、 false を返します。 コンポーネントが v-show によって非表示になっているかアサートすることに使用することができます。 Wrapper | Vue Test Utils

  it("should hide details when rendered", async () => {
    const wrapper = mount(CustomCard, { localVue, vuetify });
    const details = wrapper.find(".v-card__text");

    expect(details.isVisible()).toBe(false);
  });

ボタンをクリックする

要素.trigger(イベント名)(クリックイベントの場合は"click")でOK。
イベントの完了を待つためにawaitを使っている。

  it("should show details when icon button clicked", async () => {
    const wrapper = mount(CustomCard, { localVue, vuetify });

    const details = wrapper.find(".v-card__text");
    const button = wrapper.find(".v-btn--icon");

    await button.trigger("click");  // <= ここ

    expect(details.isVisible()).toBe(true);
  });

ソースコード

github.com