Masayan tech blog.

  1. ブログ記事一覧>
  2. 【Vitestで始める単体・結合テスト】Vueコンポーネントテストの環境構築と作成(vue-test-utils)

【Vitestで始める単体・結合テスト】Vueコンポーネントテストの環境構築と作成(vue-test-utils)

公開日

環境

  • macOS Monterey 12.6
  • VSCode
  • node.js v18.6.0
  • npm 8.13.2
  • Typescript ^5.0.2
  • vite ^4.4.5
  • vitest ^0.34.2

前置き

本記事で説明すること

以下の記事で関数テストの環境構築と実装、テストの作成、パスエイリアス、カバレッジ設定まで完了した。本記事では、Vueコンポーネントのテスト環境構築、コンポーネントの実装とテストの作成を行う

【Vitestで始める単体・結合テスト】関数テストの環境構築と作成

reactを使用したケースはこちら

【Vitestで始める単体・結合テスト】ReactコンポーネントとカスタムHooksのテストの環境構築と作成(testing-library/react)

testing-library/vueを使用したケースはこちら

【Vitestで始める単体・結合テスト】VueコンポーネントとComposable関数のテストの環境構築と作成(testing-library/vue)

コンポーネントテスト全般のTIPSなどまとめはこちら

【Vitestで始める単体・結合テスト】保守性の高いテストのTIPSやポイント

構成

Vue Test Utils

  • コンポーネントテスト用
  • DOMのレンダー、要素の取得、ユーザーのイベント処理用のメソッドなどを提供

Vue3

  • UIフレームワーク

コンポーネントテスト

環境構築

コンポーネントテスト用の依存関係を追加

  • @vue/test-utils
    • コンポーネントテスト用のライブラリ
  • happy-dom
    • 本来ブラウザでしかDOMを使用できないが、サーバーサイドなどで仮想的にDOMツリーを生成するためのライブラリ
npm install -D @vue/test-utils happy-dom

Viteの設定ファイル(vite.config.ts)調整

Domを単体テストの中で扱えるように、environmentにhappy-domを追加

/// <reference types="vitest" />
import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [vue()],
  test: {
    globals: true,
    alias: {
      "@": path.join(__dirname, "/src"),
    },
    environment: "happy-dom", // here
  },
});

コンポーネントの実装

対象のコンポーネント

以下3種類についてコンポーネントとテストをそれぞれ作成する。

  • inputコンポーネントを作成し、文字入力イベントを検証する
  • Formコンポーネントを作成し、文字入力とボタンのクリックイベントを検証する
  • TodoListコンポーネントを作成し、クリックイベントとAPI通信を検証する

inputコンポーネント

labelやid、valueとvalueを変更するための関数onChangeなどをpropsとして受け取る。inputに対して入力されるとonChangeが発火する

src/components/TextInput.vue

interface TextInputProps {
  label: string;
  id: string;
  name?: string;
  value: string;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const TextInput: React.FC<TextInputProps> = ({
  label,
  id,
  name,
  value,
  onChange,
}) => {
  return (
    <div className="mb-4">
      <label
        className="block text-gray-700 text-sm font-bold mb-1"
        htmlFor={id}
      >
        {label}
      </label>
      <input
        name={name}
        type="text"
        id={id}
        className="w-full px-3 py-2 border rounded-lg border-gray-300 focus:outline-none focus:ring focus:border-blue-300"
        value={value}
        onChange={onChange}
      />
    </div>
  );
};

export default TextInput;

Formコンポーネント

src/components/Form.vue

<script setup lang="ts">
import TextInput from "@/components/TextInput.vue";
import { ref } from "vue";

const inputValue = ref<string>("");
const submitted = ref<boolean>(false);
const message = ref<string>("");

const submit = (inputValue: string) => {
  message.value = inputValue;
  submitted.value = true;
};
</script>

<template>
  <section>
    <p v-if="submitted" data-testid="submitted-message">
      {{ `${message}を送信しました` }}
    </p>
    氏名:<TextInput v-model="inputValue" />
    <div style="margin-top: 20px">
      <button
        @click="submit(inputValue)"
        style="background-color: white; color: #000; padding: 5px 10px"
      >
        送信する
      </button>
    </div>
  </section>
</template>

TodoListコンポーネント

依存関係の追加

axiosをインストール

npm install axios

src/components/TodoList.vue

<script setup lang="ts">
import axios from "axios";
import { ref } from "vue";

// ほんとは型定義ファイルを作るべき
export type Todo = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

const todos = ref<Todo[]>([]);

const fetchTodos = async () => {
  axios
    .get("https://jsonplaceholder.typicode.com/todos")
    .then((res) => {
      console.log(res.data);

      todos.value = res.data;
    })
    .catch((err) => {
      // デモなので割愛
      console.log(err);
    });
};
</script>

<template>
  <button @click="fetchTodos">記事取得</button>
  <ul>
    <li v-for="todo in todos">{{ todo.title }}</li>
  </ul>
</template>

画面上ではこのようなかんじ

テスト作成

inputコンポーネントのテスト

  • mountメソッドによりTextInputコンポーネントをレンダリングする
  • mountメソッドの返り値から描画された Vue コンポーネントを含む Wrapperを取得できる
  • Wrapperに対してfindやfindAllなどの要素取得のメソッドが使用できる
  • input要素に対してsetValueで任意の文字列を入力する
    • インタラクティブな操作は非同期処理なのでasync awaitにする必要があるので注意
  • 最後に、期待する文字列がinput要素のvalueとして保持されているか、update:modelValueが期待する回数呼び出されているかなどをアサートする

tests/components/TextInput.test.ts

import { mount } from "@vue/test-utils";
import TextInput from "@/components/TextInput.vue";

describe("TextInput", () => {
  it("テキストインプットに文字を入力できる", async () => {
    const wrapper = mount(TextInput);
    const input = wrapper.find("input[type='text']");

    await input.setValue("大阪太郎");

    // ↓wrapper.emitted()の返り値
    // {
    //     'update:modelValue': [ [ '大阪太郎' ] ],
    //     input: [ [ [Event] ] ],
    //     change: [ [ [Event] ] ]
    // }
    console.log(wrapper.emitted());

    // setValueしてinputvalueを確認するだけではテストとして足りない。emittedでv-modelが機能していることを検証する
    expect(wrapper.emitted("update:modelValue")).toBeTruthy(); // 未発火の場合はundefinedとなりfalsyなので失敗する
    expect(wrapper.emitted("update:modelValue")).toHaveLength(1); // 発火回数
    expect(wrapper.emitted("update:modelValue")?.at(0)).toEqual(["大阪太郎"]); //値
  });
});

Formコンポーネントのテスト

tests/components/Form.test.ts

  • mountメソッドによりFormコンポーネントをレンダリングする
  • mountメソッドの返り値から描画された Vue コンポーネントを含む Wrapperを取得できる
  • inputタグへの文字入力はすでに説明済みなので割愛
  • 送信ボタンの要素を取得しクリックイベントを発火させる(trigger)
  • 最後に、期待する文字列が画面上に表示されているかアサートする
import Form from "@/components/Form.vue";
import { mount } from "@vue/test-utils";

describe("Form", () => {
  it("Formに文字を入力し送信すると、入力した内容が画面上にメッセージとして表示", async () => {
    const wrapper = mount(Form);

    const input = wrapper.find("input[type='text']");
    await input.setValue("京都花子");

    const submitButton = wrapper.find("button");
    await submitButton.trigger("click");

    const submittedMessage = wrapper.find('[data-testid="submitted-message"]');
    expect(submittedMessage.text()).toBe("京都花子を送信しました");
  });
});

TodoListコンポーネントのテスト

  • vitestのvi.spyOnによりaxiosのgetメソッドをモックにし、返り値を固定化する
  • jsonplaceholderよりtodoリストのデータを取得する処理をモックに置き換える
  • mountメソッドによりTodoListコンポーネントをレンダリングする
  • TODO取得ボタンの要素を取得しクリックイベントを発火させる
  • 最後にアサート
    • モック関数が期待する引数を与えられて呼び出されたか
    • Todoのタイトルが画面上に表示されているか

tests/components/TodoList.test.ts


import TodoList from "@/components/TodoList.vue";
import { mount } from "@vue/test-utils";

import axios from "axios";

describe("TodoList", () => {
  it("送信ボタンを押すとTODOリストの一覧が表示", async () => {
    const mockTodos = [
      { userId: 1, id: 1, title: "title1-1", completed: false },
      { userId: 1, id: 2, title: "title1-2", completed: true },
      { userId: 2, id: 1, title: "title2-1", completed: false },
      { userId: 3, id: 1, title: "title3-1", completed: true },
      { userId: 3, id: 2, title: "title1-2", completed: false },
    ];
    const axiosGetMock = vi
      .spyOn(axios, "get")
      .mockResolvedValue({ data: mockTodos });

    const wrapper = mount(TodoList);

    const submitButton = wrapper.find("button");
    await submitButton.trigger("click");

    expect(axiosGetMock).toHaveBeenCalledWith(
      "https://jsonplaceholder.typicode.com/todos"
    );

    expect(axiosGetMock).toHaveBeenCalledOnce();

    // リアクティブ値の更新後、DOMの更新を待つ
    await wrapper.vm.$nextTick();

    expect(wrapper.findAll("li").length).toBe(5);
  });
});

実行

npm run test

全てのテストが通っていればOKです。

composable関数のテスト

Vue3のComposition API を活用して状態を持つロジックをカプセル化して再利用するための関数

composables関数の実装

テスト対象の関数。カウンターをプラスしたりマイナスしたりする

src/composables/counter.ts

import { ref } from "vue";

export function useCounter() {
  const counter = ref(0);

  function inc() {
    counter.value++;
  }

  function dec() {
    counter.value--;
  }

  return { counter, inc, dec };
}

テスト作成

  • composable関数を実行する
  • リアクティブな値が更新されているか等をアサートする

tests/composables/counter.test.ts

import useCounter from "@/composables/counter";
import { describe, expect, it } from "vitest";

describe("composable関数: useCounter", () => {
  it("カウンターをインクリメントとデクリメントできる", () => {
    const { counter, inc, dec } = useCounter();

    inc();

    dec();

    inc();
    inc();

    expect(counter.value).toEqual(2);
  });
});

実行

npm run test

全てのテストが通っていればOK。

まとめ

いかがでしたでしょうか。本記事では、viteベースのvitestでvue-test-utilsを用いてvueコンポーネントの実装と単体・結合テスト、composable関数の環境構築、テストの作成などについて説明しました。コンポーネントテストを行うために必要な依存関係や設定が少し複雑なのと、ネット上にはまだそれほど充実した記事はないので、ぜひ参考にして構築してみてください。