VitestとJSDOMで実現するAstroコンポーネントテスト

高速テストフレームワークVitestと、ブラウザ環境をシミュレートするJSDOMを組み合わせ、Astroプロジェクトで堅牢なコンポーネントテストを構築する手法を解説します。

約46分で読めます
#Astro#Vitest#JSDOM#Testing#TypeScript#Unit Testing

VitestとJSDOMによるテスト戦略

Astroプロジェクトの品質を維持・向上させるためには、堅牢なテスト戦略が不可欠です。今回は、高速なテストランナーであるVitestと、Node.js環境でブラウザのDOMをシミュレートするJSDOMを組み合わせたテスト環境の構築と実践について、このブログのCalendarUtils.tsのテストを例に解説します。

なぜVitestとJSDOMなのか?

Vitestの利点

  1. 高速性: ViteのHMR(ホットモジュールリプレイスメント)エンジンを活用し、非常に高速なテスト実行を実現します。
  2. 簡単な設定: Viteプロジェクトとの親和性が高く、多くの場合、最小限の設定で導入できます。
  3. Jest互換のAPI: describe, test, expectなど、Jestに慣れた開発者ならすぐに使いこなせます。
  4. TypeScript/ESMサポート: 最新のJavaScriptエコシステムに標準で対応しています。

JSDOMの役割

AstroやReactなどのコンポーネントは、最終的にブラウザのDOM上でレンダリングされ、動作します。しかし、テストは通常Node.js環境で実行されるため、documentwindowといったブラウザ固有のグローバルオブジェクトが存在しません。

JSDOMは、この問題を解決します。Node.js内でDOM環境をシミュレートし、コンポーネントのレンダリング、イベント発火、DOM操作などのテストを可能にします。

テスト環境のセットアップ

このプロジェクトでは、vitest.config.jsでテスト環境を設定しています。

// vitest.config.js
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true
  }
});

実践例:CalendarUtils.tsの単体テスト

CalendarUtils.tsは、DOM操作を含まない純粋なデータ処理ロジックです。そのため、厳密にはJSDOM環境は不要ですが、プロジェクト全体でテスト環境を統一するためにjsdomが設定されています。

以下はsrc/utils/CalendarUtils.test.tsからの抜粋です。

// src/utils/CalendarUtils.test.ts
import { describe, test, expect } from 'vitest';
import {
  generateCalendarData,
  type Post,
} from './CalendarUtils';

describe('generateCalendarData', () => {
  test('空の投稿配列を正常処理', () => {
    const result = generateCalendarData([]);
    expect(result.totalPosts).toBe(0);
    expect(result.activeDates).toEqual([]);
    expect(result.postsByDate).toEqual({});
  });

  test('無効な日付形式を無視', () => {
    const invalidPosts: Post[] = [{
      frontmatter: { 
        pubDate: 'invalid-date',
        title: 'Test',
        tags: []
      },
      url: '/test'
    }];
    const result = generateCalendarData(invalidPosts);
    expect(result.totalPosts).toBe(0);
    expect(result.activeDates).toEqual([]);
  });

  test('正常な投稿データを正確に集計', () => {
    const validPosts: Post[] = [
      {
        frontmatter: {
          pubDate: '2025-06-29T19:30:00',
          title: 'Post 1',
          tags: ['test']
        },
        url: '/post1'
      }
    ];
    
    const result = generateCalendarData(validPosts);
    expect(result.totalPosts).toBe(1);
    expect(result.activeDates).toEqual(['2025-06-29']);
    expect(result.postsByDate['2025-06-29'].count).toBe(1);
  });
});

このテストでは、Vitestが提供するdescribeでテストスイートを定義し、test(またはit)で個別のテストケースを記述しています。expectとマッチャー(.toBe(), .toEqual()など)を使って、関数の実行結果が期待通りであるかを検証します。

テストの実行は以下のコマンドで行います:

npm test

Astroコンポーネントのテスト(今後の展望)

CalendarUtils.tsのようなロジック層のテストに加えて、JSDOMの真価が発揮されるのがコンポーネントテストです。

例えば、CalendarComponent.astroをテストする場合、Astro公式が推奨する@testing-library/astroを利用するのが一般的です。以下に仮想的なテストコードを示します。

// CalendarComponent.test.ts (仮想的な例)
import { render, screen } from '@testing-library/astro';
import CalendarComponent from '../components/CalendarComponent.astro';
import { expect } from 'vitest';
import '@testing-library/jest-dom'; // .toBeInTheDocument()などのマッチャーを利用するために必要

test('カレンダーが表示されること', async () => {
  const posts = [/* ... test data ... */];
  
  // JSDOM環境でコンポーネントをレンダリング
  // Astroコンポーネントは非同期にレンダリングされることがあるため、awaitを使用
  await render(<CalendarComponent posts={posts} />);

  // レンダリング結果をDOMから取得して検証
  const calendarTitle = screen.getByText(/投稿カレンダー/);
  expect(calendarTitle).toBeInTheDocument();

  const totalPostsStat = screen.getByText(/総投稿数:/);
  expect(totalPostsStat.textContent).toContain('10'); // 例
});

この(仮想的な)テストでは、@testing-library/astro を使ってコンポーネントをJSDOM内にレンダリングし、screenオブジェクトを通じてDOM要素を取得・検証します。Astroコンポーネントのテストでは、renderが非同期処理になることがあるためawaitを使うのがポイントです。これにより、UIが正しく表示されているか、ユーザーインタラクションが期待通りに機能するかを確認できます。

まとめ

このブログでも、CalendarUtils.tsの単体テストを皮切りに、今後はコンポーネントレベルのテストを拡充し、より品質の高いサイトを目指していきます。