vite+Vue3 単体テストを書いてみる

WebアプリケーションのフロントエンドフレームワークとしてVue3をよく使用しています。
ただ、テストコードまでは作成できておらず、コードの品質を担保することが出来ていませんでした。
今回は、公式の Vue Test Utils にあるチュートリアルを実践してみます。

テストコードを書くより、テストを走らせるまでの準備が大変でした。

A Crash Course | Vue Test Utils (vuejs.org)

実行環境

$ node -v
v18.14.0
$ npm --version
9.4.2

プロジェクト作成

vite を使用し、Vue3のプロジェクトを作成します。

# プロジェクト作成
$ npm create vite@latest
✔ Project name: … vue-test-project
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript

# 開発サーバー起動
$ cd vue-test-project
$ npm install
$ npm run dev
> vue-test-project@0.0.0 dev
> vite

  VITE v4.1.2  ready in 185 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

起動し、指定されたURLにアクセスします。

テスト用ライブラリの追加

Vueコンポーネントテスト

まずは、公式のVue Test Utils を参考にインストールします。

Installation | Vue Test Utils (vuejs.org)

$ npm install --save-dev @vue/test-utils

テストランナーで推奨されているjestをインストール

次にインストールするvue-jestに合わせるため、バージョンを固定します。

$ npm install --save-dev jest@28.1.3

jestでVueコンポーネント(.vue)をテストする場合、vue-jestというtransformerが必要という事でした。調べてもわからなかったのですが、おそらくテスト用にファイルを変換してくれるものだと思います。

こちらはVuejestのバージョンによって、パッケージとバージョンが決まっているようでした。今回はVue3 , jest@28.x なので@vue/vue3-jest@28.xをインストールしています。

GitHub - vuejs/vue-jest: Jest Vue transformer

$ npm install --save-dev @vue/vue3-jest@28

次に、テストに使用するテスト環境のためののライブラリです。 一度テストを実行したときのエラーにかかれてあったため、追加でインストールしました。 デフォルトではnodeの環境を使用するが、コンポーネントのテストをする場合はjsdomが必要なようです。

DOM 操作 · Jest

npm install --save-dev jest-environment-jsdom

ここでVueコンポーネントのテストに必要なライブラリは揃いました。

以下は不要なのですが、JavaScriptコードをテストするための準備になります。

JavaScriptテスト

jestでES6(ES2015)で書かれたJavaScriptコードをテストするためには、BabelをCommonJSに変換が必要だそうです。この変換を行うためbabel-jestをインストールします。@babel/coreはbabel-jestを使用するためにインストールしています。

babel-jest - npm

$ npm install --save-dev babel-jest@28
$ npm install --save-dev @babel/core

babelのプラグインでコードの変換する際のターゲット環境を設定できます。この環境のプリセットを設定できるライブライをインストールします。

 npm install --save-dev @babel/preset-env

Jestの設定

package.jsonにscriptsでテスト実行コマンドを追加します。 今回は/testsディレクトリ以下にテストコードを作成していくため、jest testsとしています。

{
  "scripts": {
    "test:unit": "jest tests"
  }
}

他の設定もpackage.jsonにまとめて追加します。jest.config.jsonという名前で別ファイルを準備してもいいみたいです。

  • testEnvironment: テスト環境
  • testEnvironmentOptions: テスト環境オプション
  • moduleFileExtensions: モジュールが使用するファイル拡張子
  • transform: トランスフォーマーが変換するファイルの指定
  • moduleNameMapper: 正規表現でimportされたモジュール名にマッチするものを置き換え
    • import Component from ../../src/components/HelloWorld と書いていたが src/ までのパスを@/で書くことが出来るようになります。
{
    "jest": {
        "testEnvironment": "jsdom",
        "testEnvironmentOptions": {
          "customExportConditions": [
            "node",
            "node-addons"
          ]
        },
        "moduleFileExtensions": [
          "js",
          "json",
          "vue"
        ],
        "transform": {
          ".*\\.(js)$": "babel-jest",
          ".*\\.(vue)$": "@vue/vue3-jest"
        },
        "moduleNameMapper": {
          "^@/(.*)$": "<rootDir>/src/$1"
        }
      }
}

Jestの設定 · Jest

Babelの設定

babelの設定もpackage.jsonに書いていきます。babel.config.jsという名前で別ファイルを準備してもいいみたいです。

  • presets: プリセットとして利用するプラグインのリスト
    • node: current とすることで、現在のnode.jsに合わせたコードに変換をしてくれる
{
  "babel": {
    "presets": [
      [
        "@babel/preset-env",
        {
          "targets": {
            "node": "current"
          }
        }
      ]
    ]
  }
}

Presets · Babel

これでやっとテストの準備ができました。(実際は何度もテスト実行し、足りていないライブラリや、設定を追加していきました。)

テストコード作成

ここからはチュートリアルに従って、テストコードを書いていきます。Options APIのスタイルで書かれていたが、新しいComposition APIのスタイルに変更しています。 本来はテストコードを作成し、そのテストが通るように修正していくのですが、こちらは全ての修正が完了した状態になります。

テスト対象コンポーネント

/src/components/TodoApp.vue

<script setup>
import { ref } from "vue"
const todos = ref([
    {
        id: 1,
        text: "Learn Vue.js 3",
        completed: false
    }
]);

const newTodo = ref("");

const createTodo = () => {
    todos.value.push(
        {
            id: 2,
            text: newTodo,
            completed: false
        }
    );
};
</script>
<template>
    <div>
        <div v-for="todo in todos" :key="todo.id" data-test="todo" :class="[todo.completed ? 'completed' : '']">
            {{ todo.text }}
            <input type="checkbox" v-model="todo.completed" data-test="todo-checkbox" />
        </div>

        <form data-test="form" @submit.prevent="createTodo">
            <input data-test="new-todo" v-model="newTodo" />
            <input type="submit">
        </form>
    </div>
</template>

テストコード

/tests/unit/TodoApp.spec.js

import { mount } from "@vue/test-utils"
import TodoApp from "@/components/TodoApp"

test("renders a todo", () => {
    const wrapper = mount(TodoApp);

    const todo = wrapper.get("[data-test='todo']");

    expect(todo.text()).toBe("Learn Vue.js 3");
})

test("creates a todo", async () => {
    const wrapper = mount(TodoApp);

    await wrapper.get("[data-test='new-todo']").setValue("New todo");
    await wrapper.get("[data-test='form']").trigger("submit");

    expect(wrapper.findAll("[data-test='todo']")).toHaveLength(2);
})

test("completes a todo", async () => {
    const wrapper = mount(TodoApp);

    await wrapper.get("[data-test='todo-checkbox']").setValue(true);

    expect(wrapper.get("[data-test='todo']").classes()).toContain("completed");
})

テスト実行

$ npm run test:unit

> vue-test-project@0.0.0 test:unit
> jest tests

 PASS  tests/unit/TodoApp.spec.js
  ✓ renders a todo (21 ms)
  ✓ creates a todo (8 ms)
  ✓ completes a todo (5 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.887 s, estimated 1 s
Ran all test suites matching /tests/i.

カバレッジ出力

簡単な見方としては

  • Stmts: C0 実行された行の網羅率
  • Branch: C1 ifなのど分岐された処理の網羅率
  • Funcs: 各関数呼び出し網羅率
  • Lines: ファイルの各実行可能行が実行されたかの網羅率。Stmts と同じ?
$ npm run test:unit -- --coverage

> vue-test-project@0.0.0 test:unit
> jest tests --coverage

[vue-jest]: Not found tsconfig.json.

 PASS  tests/unit/TodoApp.spec.js
  ✓ renders a todo (22 ms)
  ✓ creates a todo (8 ms)
  ✓ completes a todo (4 ms)

-------------|---------|----------|---------|---------|-------------------
File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------|---------|----------|---------|---------|-------------------
All files    |     100 |      100 |     100 |     100 |                   
 TodoApp.vue |     100 |      100 |     100 |     100 |                   
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.903 s, estimated 1 s
Ran all test suites matching /tests/i.

/coverage/lcov-report/index.htmlにレポートも出力されるようです。便利。