Vitestでフロントエンドテストを始める：実践的な書き方とテスト観点の備忘録

# はじめに

こんにちは、Gakken Leap のフロントエンドエンジニアの katashima です。

私は3ヶ月ほど前にフロントエンドのNuxt.js開発に参画しました。参画当初はテストコードを書くことに慣れておらず、以前にJestを少し使った経験はあるものの、Vitestは初めてでした。

Vitestに限らず、テストフレームワークについてまだまだ学習中ですが、基本的な書き方とテスト観点について備忘録として残しておきたいと思います。

# Vitestとは？なぜVitestを選ぶのか

Vitestは、フロントエンドビルドツールである**Vite**を基盤としたテストフレームワークです。

## Vitestの主な特徴

1. **高速なテスト実行**: Viteの「開発サーバーの起動が速い」「HMR（Hot Module Replacement）が高速」といったメリットを、テスト実行時にも享受できます
2. **Jest互換性**: describe や it（test）、expect といった基本的なAPIはもちろん、モック化の方法などもほぼ同じ感覚で利用できます
3. **Viteエコシステムとの親和性**: 設定ファイルや依存関係を共有でき、セットアップが簡単です

これらの特徴により、「Jestを使った経験はあるけど、Vitestは初めて」という私でも、スムーズに移行してすぐにテストコードを書き始めることができました。

# プロジェクトのテストの構造に関する解説（例）

## 1. テストの主要ディレクトリ構造

```text
test/
├── fixtures/       - テストデータのモックオブジェクト
├── integration/    - 統合テスト
├── unit/           - 単体テスト
└── utils/          - テストのヘルパー関数
```

## 2. テストの種類

### 単体テスト (unit/)

個々のコンポーネントや関数を分離してテストします。主に以下のカテゴリで構成されています：

- コンポーネントのテスト: unit/components/
  - 例: Button.spec.ts では単一コンポーネントの振る舞いをテスト
- コンポーザブルのテスト: unit/composables/
  - 例: useAuth.spec.ts では特定のコンポーザブル関数の振る舞いをテスト

### 統合テスト (integration/)

複数のコンポーネントやページ全体の相互作用をテストします：

- PC 版のテスト: pc.spec.ts というファイル名パターン
  - 例: login-pc.spec.ts (PC ログインページ), dashboard-pc.spec.ts (PC 管理画面)
- モバイル版のテスト: mobile.spec.ts というファイル名パターン
  - 例: login-mobile.spec.ts (モバイルログインページ), dashboard-mobile.spec.ts (モバイル管理画面)

**PCとモバイルの差分について：**

- レイアウトやナビゲーション構造が異なるため、個別のテストファイルで対応
- 基本的な機能が同じで、UIのみ異なる場合が多いため、共通部分はうまく抽出してテストを行うよう検討中
- 例：ログイン機能は同じだが、PCはサイドバー、モバイルはハンバーガーメニューといった違い

PCとモバイルでレンダリングするコンポーネントを切り替える例：

```javascript
if (device === "pc") {
  render({
    components: { PC },
    template: `
      <div>
        <PC />
      </div>
    `,
  });
} else {
  render({
    components: { Mobile },
    template: `
      <div>
        <Mobile />
      </div>
    `,
  });
}
```

### テストフィクスチャ (fixtures/)

テストで使用するデータモックが含まれています。各ドメインオブジェクト別にファイルが分かれています：

- ユーザー関連: `users.ts`
- 投稿関連: `posts.ts`
- 例: `users.ts` ではユーザーデータのモックオブジェクトを定義

# Vitest の基本的な書き方

基本的な構造は以下がわかりやすいので参考にしてください
https://qiita.com/lilacs/items/7babaf941af067e818d0

## テストのパターン

主に以下のテストパターンの構造で記述しています。

### 単体テストの記述ブロック構造（例）

```javascript
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/vue";
import { describe, test, expect, beforeEach, vi, afterEach } from "vitest";

describe("大分類(Button.vueのコンポーネントテスト)", () => {
  // セットアップ関数
  const setup = (props = {}) => {
    const defaultProps = {
      // デフォルトのpropsを定義
      size: "large",
      color: "primary",
      componentStyle: "filled",
      text: "プライマリボタン",
    };
    return render(Button, {
      props: { ...defaultProps, ...props }, // コンポーネントのpropsを設定
    });
  };
  beforeEach(() => {
    // テストの前に実行する処理
    // 例: タイマーのモックを設定
    vi.useFakeTimers();
  });
  afterEach(() => {
    // テストの後に実行する処理
    // 例: モックの状態をクリア
    vi.clearAllMocks();
  });
  describe("中分類(初期表示)", () => {
    beforeEach(() => {
      // テストの前に実行する処理
      setup(); // コンポーネントをレンダリング
    });
    test("正しい文字列が表示される", () => {
      // 検証コード
      // 例: ボタンのテキストが正しいか確認
      expect(
        screen.getByRole("button", { name: "プライマリボタン" })
      ).toBeInTheDocument();
    });
  });
  describe("中分類(プロパティ変更)", () => {
    beforeEach(() => {
      // テストの前に実行する処理
      setup({ text: "セカンダリボタン", color: "secondary" }); // プロパティを変更してコンポーネントをレンダリング
    });
    test("プロパティ変更時", () => {
      // 検証コード
      // 例: ボタンのテキストが正しいか確認
      expect(
        screen.getByRole("button", { name: "セカンダリボタン" })
      ).toBeInTheDocument();
    });
  });
  describe("中分類(イベントの発火)", () => {
    ... // イベントの発火に関するテスト
  });
  ... // 他の中分類のテスト
});
```

### アクセシビリティについて

テスト観点で特に気をつけたい点として、アクセシビリティの観点を確認しています：

- **ARIA 属性の確認**: コンポーネントが適切な ARIA 属性を持っているか
  - 例: ボタンコンポーネントが `role="button"` を持つか
ボタンロールを持つことでキーボード操作やスクリーンリーダーでの読み上げが適切に行われることを確認できる

```javascript
// なるべくgetByTextは使わない
expect(screen.getByText("プライマリボタン")).toBeInTheDocument();

// getByRoleを使う
expect(screen.getByRole("button", { name: "プライマリボタン" })).toBeInTheDocument();
```

**なぜgetByRoleを推奨するのか：**

| 要素 | キーボード操作 | スクリーンリーダー | 推奨度 |
|------|----------------|-------------------|---------|
| `<button>` | ✅ Tabキーで選択可能 | ✅ 適切に読み上げ | 推奨 |
| `<div>` | ❌ Tabキーで選択不可 | ❌ 意味を理解できない | 非推奨 |

`<div>`タグでもボタンを表現できますが、Tabキーで操作できません。[セマンティック](https://developer.mozilla.org/ja/docs/Glossary/Semantics)なHTMLを使用することで、アクセシビリティを向上させることができます。

`getByText`は、テキストの内容で要素を取得するため、アクセシビリティの観点からはあまり推奨されません。代わりに、`getByRole`を使用して、要素の役割（role）を指定することで、より適切なアクセシビリティテストが可能になります。

buttonタグのアクセシビリティ
![スクリーンショット 2025-06-13 14.39.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3712998/63fe2ef0-ad14-4c5a-b4e5-630b126940e4.png)

divタグのアクセシビリティ
![スクリーンショット 2025-06-13 14.47.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3712998/13a78478-dac4-4eae-9b6c-86e80a54addc.png)

### その他テストの観点

テストを書く際は以下の観点を見ると良さそうです：

#### 基本的な動作確認

- **初期表示**: コンポーネントが正しくレンダリングされるか
  - 例: ボタンのテキスト、スタイル、アイコンなどが期待通りに表示されるか
- **プロパティの変更**: propsや状態が変更された際の挙動
  - 例: ボタンのテキストが変更された場合、正しく更新されるか
- **イベントの発火**: ユーザー操作に対するイベントが正しく発火するか

#### エラーハンドリングと品質

- **エラーハンドリング**: 不正な入力や状態に対する挙動
  - 例: 必須のプロパティが欠けている場合、適切なエラーメッセージが表示されるか
- **依存関係のモック化**: 外部APIやサービスとの依存関係をモック化して、テストの独立性を保つ
  - 例: APIからのレスポンスをモックして、特定のデータが返されることを確認

#### アクセシビリティと相互作用

- **アクセシビリティ**: ARIA属性やキーボード操作のサポート
  - 例: ボタンがキーボードで操作可能か、スクリーンリーダーで正しく読み上げられるか
- **コンポーネント間の相互作用**: 複数のコンポーネントが連携する場合、その相互作用が正しく行われるか

#### パフォーマンスと外観

- **非同期処理**: API呼び出しやタイマーなどの非同期処理が正しく動作するか
  - 例: APIからのデータ取得後にコンポーネントが正しく更新されるか
- **スタイルの適用**: CSSやスタイルが正しく適用されているか
- **ユニットテストのカバレッジ**: 各関数やメソッドが適切にテストされているか
  - 例: ボタンコンポーネントのクリックイベントが正しく処理されるか

# まとめ

## Vitestの活用ポイント

- **高速性、Jest互換性、Viteエコシステムとの親和性**により、モダンなフロントエンド開発において強力なテストツール
- アクセシビリティを意識したテスト設計により、より良いユーザー体験を提供

## テストの継続的な改善

- テストは「書く」だけでなく「継続する」ことが重要
- カバレッジ率は目安の一つ：参画当初60% → 現在79%（80%を目標に継続中）
- 今回紹介したテスト観点は引き続き意識し、開発効率と品質向上を目指していきます

# エンジニア募集中

Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています！！
ぜひお気軽にカジュアル面談へお越しください！！

https://gakken-leap.co.jp/recruit/