【Astro】Astro ActionsをVitestでテストする方法

【Astro】Astro ActionsをVitestでテストする方法

Astro v4.5から導入されたAstro Actionsはサーバーサイドのロジックを集約するのに便利ですが、そのロジックをどうやってテストするかで少し試行錯誤しました。

Astro #Astro#Vitest#Astro Actions

【Astro】Astro ActionsをVitestでテストする方法

サムネイル

Astro v4.5から導入されたAstro Actionsはサーバーサイドのロジックを集約するのに便利ですが、そのロジックをどうやってテストするかで少し試行錯誤しました。

更新日: 7/25/2025

課題

ISSUE - 課題

Vitestがastro:actionsを名前解決できない

当初、Vitestの標準的な設定 (import { defineConfig } from 'vitest/config') でテスト環境を構築しようとしました。しかし、import { defineAction } from 'astro:actions'; のようなAstro独自のモジュールをVitestが解決できず、テストを実行することができませんでした。

SOLUTION - 解決策

getViteConfigとモックの活用

vitest.config.tsでAstroが提供するgetViteConfigを利用することで解決できました。さらに、astro:actionsastro:schemaをモックすることで、Astroのランタイムに依存しないテストが可能になります。

1. Vitestの設定

まず、必要なパッケージをインストールします。

pnpm add -D vitest zod

次に、vitest.config.tsを以下のように修正しました。defineConfigの代わりにgetViteConfigを使い、テストのセットアップファイルをsetupFilesに指定します。

import { getViteConfig } from 'astro/config';
import tsconfigPaths from 'vite-tsconfig-paths';

export default getViteConfig({
  plugins: [tsconfigPaths()],
  test: {
    setupFiles: ['src/__tests__/setup.ts'], // 共通モックファイルを指定
    include: ['src/**/*.{test,spec}.ts'],   // テストファイルの場所を指定
  },
});

2. Astro独自モジュールのMock

setupFilesで指定したsetup.tsに、Astro独自モジュールの処理を定義します。

ここでの重要なポイントは、defineActionhandler関数を直接返すようにモックすることです。これにより、テストコードからビジネスロジックが書かれたhandlerを直接呼び出す形になります。

import { vi } from 'vitest';

// 'astro:schema'は、実際の'zod'を返すようにモック
vi.mock('astro:schema', async () => {
  const zod = await vi.importActual('zod');
  return {
    z: zod.z,
  };
});

// 'astro:actions'のdefineActionがhandler関数を直接返すようにモック
vi.mock('astro:actions', async (importOriginal) => {
  const original = await importOriginal<typeof import('astro:actions')>();
  return {
    ...original,
    defineAction: ({ handler }: { handler: (...args: any[]) => any }) => {
      return handler; // handlerをそのまま返すが、バリデーションのテストをする場合はinputも含める必要あり
    },
  };
});

3. テストコードの実装

テスト対象として、FormDataを扱うcreateUserと、JSONオブジェクトを扱うgetUserという2つのActionを用意しました。

import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';

export const userActions = {
  createUser: defineAction({
    accept: 'form',
    input: z.object({ /* ... */ }),
    handler: async (formData: FormData, context) => {
      const name = formData.get('name');
      if (name === 'ErrorUser') {
        throw new Error('Failed to create user due to a simulated error');
      }
      return { message: 'User created successfully!!' };
    },
  }),
  getUser: defineAction({
    accept: 'json',
    input: z.object({ /* ... */ }),
    handler: async ({ id }, context) => {
      if (id === 'not-found') {
        return new Error('User not found');
      }
      return { id, name: 'Test User', email: '[email protected]' };
    },
  }),
};

テストコードでは、このuserActionsを直接インポートするようにしています。defineActionのモックにより、userActions.createUseruserActions.getUserは実質的にhandler関数そのものになっています。

import { describe, it, expect, beforeEach } from 'vitest';
import { userActions } from '@/actions/user'; // userActionsを直接インポート
import { createActionMocks } from '@/__tests__/helpers'; // contextを生成するヘルパー

describe('Astro Actions', () => {
  let mockContext: ReturnType<typeof createActionMocks>;

  beforeEach(() => {
    mockContext = createActionMocks();
  });

  describe('createUser action', () => {
    it('should create a user successfully', async () => {
      const input = new FormData();
      input.append('name', 'John Doe');
      input.append('email', '[email protected]');

      // handler関数を直接呼び出してテスト
      const result = await userActions.createUser(input, mockContext);

      expect(result).toEqual({
        message: 'User created successfully!!',
      });
    });

    it('should throw an error for "ErrorUser"', async () => {
      const input = new FormData();
      input.append('name', 'ErrorUser');
      input.append('email', '[email protected]');

      await expect(userActions.createUser(input, mockContext)).rejects.toThrow(
        'Failed to create user due to a simulated error'
      );
    });
  });

  describe('getUser action', () => {
    it('should get a user successfully', async () => {
      const input = { id: 'user-123' };
      const result = await userActions.getUser(input, mockContext);

      expect(result).toEqual({
        id: 'user-123',
        name: 'Test User',
        email: '[email protected]',
      });
    });
  });
});

まとめ

この方法で、Astroの実行環境を完全に再現することなく、Actionsに記述したサーバーサイドのロジックを安全にテストできるようになりました。getViteConfigの利用と、defineActionを適切にモックすることが、テストを実現する上での鍵でした。

検索

検索条件に一致する記事が見つかりませんでした