課題
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を適切にモックすることが、テストを実現する上での鍵でした。
コメント