✳️

Vitest入門 - ハンズオン有

2024/05/07に公開

ざっくり当記事が解説する内容
説明の際のライブラリはVueを使います。

  • Vitestの概要
  • Jestとの違い
  • Vitestの設定
  • Vitestハンズオン

VitestとJest

JestはJavaScriptのテストのためのフレームワークで、多くのJavaScript開発者に採用されています。しかし、Viteで構築されたアプリケーションとJestとの間には幾つかの技術的な隔たりが存在します。Jestは元々Node.js環境向けに設計されており、ESモジュールのサポートやその他のWeb標準技術の直接的なサポートが限定的で、以下の問題が顕著になります。

  • 設定の複雑化:
    JestをViteプロジェクトで使用するには、Viteの設定をJestが解釈できる形に変換するための追加設定が必要です。これにはBabelなどの設定や、各種プラグインの互換性を保つための調整が含まれます。

  • プラグインの非互換性:
    Viteで使用されるプラグインがJestでは正常に動作しない場合があります。これは、プラグインが依存する環境がブラウザである場合、Node.js環境のJestではうまく機能しないためです。

  • パフォーマンスの低下:
    Jestは全てのテストファイルと依存関係を事前に変換してからテストを実行するため、プロジェクトが大きくなるにつれてテストの開始までの時間が長くなります。これは特に大規模なプロジェクトで問題となり、開発の迅速化を阻害する要因となります

これらの問題を解消するために設計されたのがVitestです。
VitestはViteのエコシステムを直接利用することで、テスト環境の設定を大幅に簡素化します。
具体的には、Viteの設定ファイルをそのままテストに適用することができ、開発環境とテスト環境の差異を最小限に抑えることが可能です。また、Vitestは以下のような特徴を持っています。

  • 高速なテスト実行:
    VitestはViteのモジュール変換機能を活用してテストファイルを処理するため、Jestに比べてテストの起動と実行が速いです。

  • リアルタイムフィードバック:
    ホットリロードと同様に、Vitestもテストの変更をリアルタイムで検知し、関連するテストのみを再実行することができます。これにより、テストプロセスの効率が向上します。

Vitestを動かしてみる

まずはVitestをinstallしましょう。


プロジェクトを見てみると、vite.config.js と vitest.config.js の二つの設定ファイルが作成されています。これらのファイルのどちらにもVitestの設定を記述することができますが、vitest.config.js はより高い優先度を持ち、vite.config.js の設定を上書きします。

$ npm run test:unitでsrc フォルダ内の .test.js または .spec.js で終わるファイルを探し出し、それらのテストを実行します。

package.json

  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test:unit": "vitest",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
    "format": "prettier --write src/"
  },

テストを書いてみる

実際に適当なテストを書いてみます。
testsフォルダにtest.jsで終わるファイル名でファイルを作成します。
array.tests.js

import { describe, it, expect } from 'vitest';

describe("array sorting", () => {
    it("sorts array in ascending order", () => {
        const numbers = [5, 3, 8, 1];
        const sortedNumbers = numbers.sort((a, b) => a - b);
        expect(sortedNumbers).toEqual([1, 3, 5, 8]);
    });
});

numbers配列が正しく昇順にソートされているかのテストを書いてみました。

  • describeブロック:
    "array sorting": このテストスイートが配列のソート機能をテストすることを説明します。
    機能単位で複数のテストケースを囲う形で使用します。

  • itブロック:
    "sorts array in ascending order": 具体的なテストケースを説明します。このテストでは配列が昇順にソートされることを確認します。

テストがpassしているのが確認できますね。
Waiting for file changes...とあるように変更を検知してくれるので何度もtest実行のコマンドを打つ必要はありません。

テストが失敗している場合は、以下のように対象箇所が表示されます。

コンポーネントをテストする

以下のコンポーネントをテストしてみます。
StatusDisplay.vue

<template>
    <div :class="statusClass">{{ statusMessage }}</div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
    status: String
});

const statusClass = computed(() => {
    return `status--${props.status}`;
});

const statusMessage = computed(() => {
    if (props.status === 'success') {
        return 'Operation was successful!';
    } else if (props.status === 'error') {
        return 'An error occurred!';
    } else {
        return 'Unknown status';
    }
});
</script>

<style>
.status--success {
    color: green;
}
.status--error {
    color: red;
}
.status--unknown {
    color: gray;
}
</style>

まずはVue Test Utilsをインストールします。
これは、Vue.js向けの公式テストユーティリティライブラリで、Vueコンポーネントを隔離してテストするためのメソッドやヘルパー関数を提供します。DOMイベントのシミュレーションやコンポーネントの状態の検証などが行えます。Vue Test Utilsをインストールするには次のコマンドを実行します。

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

テストは、propsがコンポーネントにどのように影響を与えるかを検証することで、コンポーネントの正確な動作を保証することにします。

StatusDIsplay.test.js

import { describe, test, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import StatusDisplay from '../StatusDisplay.vue';

describe('StatusDisplay component', () => {
  test('renders the correct style and message for success', () => {
    const wrapper = mount(StatusDisplay, {
      props: { status: 'success' }
    });
    expect(wrapper.text()).toBe('Operation was successful!');
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['status--success']));
  });

  test('renders the correct style and message for error', () => {
    const wrapper = mount(StatusDisplay, {
      props: { status: 'error' }
    });
    expect(wrapper.text()).toBe('An error occurred!');
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['status--error']));
  });

  test('renders the correct style and message for unknown status', () => {
    const wrapper = mount(StatusDisplay, {
      props: { status: 'unknown' }
    });
    expect(wrapper.text()).toBe('Unknown status');
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['status--unknown']));
  });
});

スナップショットテスト

コンポーネントの期待される出力の「スナップショット」を撮り、テスト中に生成される実際の出力と比較することができます。
コンポーネントのHTMLまたはオブジェクトのスナップショットを生成し、これを保存されたリファレンスと比較します。予期せぬ変更があった場合、テストは失敗し、開発者はその変更を確認し、必要に応じて対応を行うことが可能になるわけです。

StatusDIsplay.test.js

describe('StatusDisplay Snap', () => {
  test('成功ステータスのスナップショット', () => {
    const wrapper = mount(StatusDisplay, {
      props: { status: 'success' }
    });
    expect(wrapper.html()).toMatchSnapshot();
  });

  test('エラーステータスのスナップショット', () => {
    const wrapper = mount(StatusDisplay, {
      props: { status: 'error' }
    });
    expect(wrapper.html()).toMatchSnapshot();
  });

  test('不明なステータスのスナップショット', () => {
    const wrapper = mount(StatusDisplay, {
      props: { status: 'unknown' }
    });
    expect(wrapper.html()).toMatchSnapshot();
  });
});

例えば以下のようにそれぞれのステータスごとにスナップショットのテストを作成しました。
すると__snapshots__フォルダ内にStatusDisplay.test.js.snapが作成されて、現在の状態が記録sれます。

この状態でコンポーネントに以下のような意図しない変更を加えてみました。

const statusMessage = computed(() => {
    if (props.status === 'success') {
        return 'Operation was successful!';
    } else if (props.status === 'error') {
        return '適当な文字列';
    } else {
        return 'Unknown status';
    }
});

するとテストはは前回のスナップショットと変更後のコンポーネントの差分を把握して、以下のようにつたえてくれるのです。

意図した変更の場合はuを押すことでスナップショットが更新されます。

Discussion