📝

vitestの導入とテスト例

2024/11/18に公開

はじめに

こんにちは。株式会社ミラボでSREチームに所属している田中です。
普段は府省庁向けのプロジェクトでアプリケーションの開発をしながら、たまにCICDパイプラインの整備などをやっています。俗に言うEmbedded SREに近い役割です。
また、技術ブログの運営も行わせていただいています。

背景

私が参加しているプロジェクトではvitestを使ってUnitTestを書いています。

私が参画したタイミングですでにテストフレームワークの選定とインストールまで完了していたため、最近までは pnpm run test してテストを追加したり動かすだけだったんですが、mockに関連するコードを触る機会があった際にmockどうやって使うんだべ?となったので、テスト環境の構築とmockを使うまでについて整理しました。

Vitestとは?

Vitest(ヴィーテスト) は、JavaScript と TypeScript のための高速で軽量なテスティングフレームワークです。主にフロントエンド開発でのユニットテストやコンポーネントテストを行う際に使用されます。Vitest は、Vite(ビルドツール)をベースにしており、非常に高いパフォーマンスを誇ります。
実はLTで今回書いてる記事の内容を発表したのですが、ヴィーテストと発音するところに驚かれている人が多かったです。

Vitestの特徴

特徴(長いのでトグル)
  1. 高速なテスト実行
  • Vitest は Vite のビルドツールをベースにしているため、非常に高速で、テストの実行やウォッチモードも快適です。
  1. Vite との統合
  • Vite のエコシステム内で動作するため、Vite プロジェクトのセットアップが簡単で、既存の Vite 設定とシームレスに統合できます。
  1. TypeScript サポート
  • TypeScript にネイティブ対応しており、TypeScript で書かれたコードをそのままテストできます。
  1. ウォッチモード
  • ファイルが変更されるたびに自動でテストを再実行するウォッチモードをサポートしており、開発中のテスト実行が便利です。
  1. スナップショットテスト
  • スナップショットテストをサポートしており、DOMの構造やコンポーネントの出力を保存して、次回実行時に比較することができます。
  1. モックとスタブ
  • テスト内で依存関係をモック(模擬)したり、スタブ(置き換え)する機能が強力です。
  1. 簡単な設定
  • 設定ファイルはシンプルで、すぐに使い始めることができます。例えば、デフォルトの設定で、すぐにテストの実行が可能です。
  1. パラレル実行
  • テストはパラレルに実行されるため、複数のテストを効率的に処理できます。

開発環境

とりあえず、下記が私の開発環境になります。

  • mac: v14.6.1
  • Vscode: v1.88.1
  • node: v22.1.0
  • pnpm: 8.15.9

プロジェクトの作成と関連パッケージのインストール

create-next-appコマンドでプロジェクトを作成します。

# Next.js プロジェクトを作成
npx create-next-app@latest vitest-sample --typescript
cd vitest-sample

作成後は関連パッケージをインストールします。

pnpm install --save-dev vitest @testing-library/react @testing-library/jest-dom jsdom @types/jest vite

Vitestの設定

次に、Vite の設定ファイルを作成し、Vitest のテスト設定を追加します。
プロジェクトのルートに vite.config.ts を作成します。

vite.config.ts
import { defineConfig } from 'vite'
import { defineConfig as defineVitestConfig } from 'vitest/config'

// Vite の設定
export default defineConfig({
  test: {
    globals: true,         // グローバル変数 (expect など) を使用するため
    environment: 'jsdom',  // React コンポーネントのテストには jsdom 環境が必要
    coverage: {
      provider: 'istanbul', // カバレッジツールをインストール
    },
  },
})

package.jsonにvitestを実行するコマンドも追加しておきます。

package.json
{
  "name": "jest-sample",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "test": "vitest"
  },

これで、ルートディレクトリでpnpm run testでvitestのテストが実行されます。
添付の画像では、テスト対象のファイルが4つで23個のテストケースがPASSになり1件Skipしていることを示しています。1件でも失敗している場合は赤色の文字でメッセージが表示されます。

Vitestによる基本的な関数のテスト

実際にテストを作成する場合の例について、テストする関数をsum.ts、テストを記載するコードをsum.test.tsに分けて例示します。

sum.ts
export function sum(a: number, b: number): number {
  return a + b;
}

sum.tsは数字型の引数を2つ受け取って、足し算した結果を返すシンプルな関数です。
テストコードは次のように書くことができます。

sum.test.ts
import { describe, it, expect } from 'vitest';
import { sum } from './sum';

describe('sum function', () => {
  it('should return 3 when adding 1 and 2', () => {
    expect(sum(1, 2)).toBe(3);
  });

  it('should return 0 when adding -1 and 1', () => {
    expect(sum(-1, 1)).toBe(0);
  });

  it('should return a negative number when adding two negative numbers', () => {
    expect(sum(-2, -3)).toBe(-5);
  });
});
  • describe: テストのグループ化を行います。ここではsum functionという名前のテストグループを作成しています。
  • it: 個々のテストケースを定義します。itは「~すべき」という形式でテストの意図を記述します。
  • expect: 期待される結果を指定します。expectで囲まれた値が、指定された条件に一致することを検証します。
    • toBe: 値が一致するかを確認するマッチャーです。この場合、sum(1, 2)が3であることを検証しています。

これでpnpm run testを実行することで、関数sum()がテストケースに対して正しい値を返しているかを検証できます。

Vitestによる応用的な関数のテスト

応用編として、1秒後に加算結果を返す非同期関数のテスト方法と、例外発生に関するテストとmockを使ったテスト方法についても例示します。

sum.ts
export async function sumAsync(a: number, b: number): Promise<number> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(a + b), 1000);
  });
}

export function throwError() {
  throw new Error('Something went wrong');
}

上記の関数は

  • sumAsync: 1秒後に加算結果を返す非同期関数です。非同期処理をテストする方法を学ぶために利用します。
  • throwError: 明示的にエラーを投げる関数です。エラーハンドリングのテストを行うために使います。
    になります。
sum.test.ts
import { describe, it, expect, vi } from 'vitest';
import { sumAsync, throwError } from './sum';

describe('sumAsync function', () => {
  it('should return 5 after 1 second when adding 2 and 3', async () => {
    const result = await sumAsync(2, 3);
    expect(result).toBe(5);
  });
});

describe('throwError function', () => {
  it('should throw an error', () => {
    expect(() => throwError()).toThrow('Something went wrong');
  });
});

describe('mocking example', () => {
  it('should call mock function and return the correct value', () => {
    // モック関数を作成し、返り値を指定
    const mockFn = vi.fn().mockReturnValue(10); // モック関数が常に 10 を返すように設定

    // モック関数を呼び出す
    const result = mockFn(1, 2);

    // モック関数が期待通りの引数で呼ばれたかを確認
    expect(mockFn).toHaveBeenCalledWith(1, 2);

    // モック関数が1回だけ呼ばれたことを確認
    expect(mockFn).toHaveBeenCalledTimes(1);

    // 返り値が期待通りであることを確認
    expect(result).toBe(10);
  });
});

  1. 非同期関数のテスト (sumAsync):
  • sumAsync関数は非同期関数です。setTimeoutで遅延を発生させてから結果を返します。
  • awaitを使って非同期の結果を待ち、expectで結果が正しいかを確認します。テストが非同期であることを示すために、テストケース自体にasyncを付けています。
  1. エラーハンドリングのテスト (throwError):
  • throwErrorは例外を投げる関数です。
  • expect(() => throwError()).toThrow('Something went wrong')のように、関数がエラーを投げることを確認します。この書き方では、throwError関数が実行される際にエラーが発生し、それが"Something went wrong"というメッセージを含むことをチェックしています。
  1. モック関数のテスト (vi.fn):
  • mockReturnValueの使用:
    • vi.fn().mockReturnValue(10)を使って、モック関数が呼ばれたときに常に10を返すように設定しました。これにより、関数の返り値もテスト対象になります。
  • 返り値の検証 (expect(result).toBe(10)):
    • モック関数が返す値が期待通りであることを確認するために、expect(result).toBe(10)を使って返り値が10であることを検証します。
  • 引数の確認 (toHaveBeenCalledWith):
    • expect(mockFn).toHaveBeenCalledWith(1, 2)で、mockFnが(1, 2)という引数で呼ばれたことを検証しています。
  • 呼び出し回数の確認 (toHaveBeenCalledTimes):
    • expect(mockFn).toHaveBeenCalledTimes(1)で、mockFnが1回だけ呼ばれたことを検証しています。

おわり

Vitestの導入手順と基本的なテスト例と非同期・例外・mockの応用的なテスト例を整理しました。ご参考になれば幸いです。

弊社ではいっしょに働いてくれる仲間を募集中です。気軽にご連絡ください!
https://mi-labo.co.jp/

Milabo Engineers Blog

Discussion