課題
Vitestがastro:actions
を名前解決できない
当初、Vitestの標準的な設定 (import { defineConfig } from 'vitest/config'
) でテスト環境を構築しようとしました。しかし、import { defineAction } from 'astro:actions';
のようなAstro独自のモジュールをVitestが解決できず、テストを実行することができませんでした。
getViteConfig
とモックの活用
vitest.config.ts
でAstroが提供するgetViteConfig
を利用することで解決できました。さらに、astro:actions
とastro: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独自モジュールの処理を定義します。
ここでの重要なポイントは、defineAction
がhandler
関数を直接返すようにモックすることです。これにより、テストコードからビジネスロジックが書かれた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.createUser
やuserActions.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
を適切にモックすることが、テストを実現する上での鍵でした。