始めに
私たちはフロントテストを行う上で効果的なAIの活用法を見つけるべく調査、検証を行った結果、私たちが得られた知見を共有します。
この記事で分かること
テストコードをAIを使って作る際に気をつけたいこと
使用ツール
- AWS Bedrock
- VScode
- Continue(VScodeの拡張機能)
やったこと
Vite+TypeScriptのプロジェクトのページコンポーネントに対してのテストコードをAIを使ってどうすれば効率よくコーディングできるかどうか、3通りのやり方を試しました。
1.3つの画面のテストコード作成を一気に依頼
プロンプト
@PostDetail.tsx @PostList.tsx @CreatePost.tsx @Vitest このアプリのフロントエンドのテストをページごとに作成してください
はい、Vitestを使ってPostDetail.tsx、PostList.tsx、CreatePost.tsxのテストを作成しましょう。各コンポーネントに対して別々のテストファイルを作成します。
PostDetail.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import PostDetail from './PostDetail'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
// Mock the AWS Amplify API client
vi.mock('aws-amplify/api', () => ({
generateClient: vi.fn(() => ({
models: {
PostMessage: {
get: vi.fn(),
list: vi.fn(),
create: vi.fn(),
update: vi.fn(),
},
},
})),
}))
describe('PostDetail', () => {
it('renders loading state', () => {
render(
<MemoryRouter initialEntries={['/post/123']}>
<Routes>
<Route path="/post/:postId" element={<PostDetail />} />
</Routes>
</MemoryRouter>
)
expect(screen.getByText('Loading...')).toBeDefined()
})
it('renders post not found', async () => {
const client = (await import('aws-amplify/api')).generateClient()
client.models.PostMessage.get.mockResolvedValue({ data: null })
render(
<MemoryRouter initialEntries={['/post/123']}>
<Routes>
<Route path="/post/:postId" element={<PostDetail />} />
</Routes>
</MemoryRouter>
)
expect(await screen.findByText('Post not found')).toBeDefined()
})
it('renders post details', async () => {
const mockPost = {
id: '123',
userName: 'TestUser',
message: 'Test message',
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
good: 5,
}
const client = (await import('aws-amplify/api')).generateClient()
client.models.PostMessage.get.mockResolvedValue({ data: mockPost })
client.models.PostMessage.list.mockResolvedValue({ data: [] })
render(
<MemoryRouter initialEntries={['/post/123']}>
<Routes>
<Route path="/post/:postId" element={<PostDetail />} />
</Routes>
</MemoryRouter>
)
expect(await screen.findByText('TestUser')).toBeDefined()
expect(screen.getByText('Test message')).toBeDefined()
expect(screen.getByText('👍 5')).toBeDefined()
})
// 他のテストケース(返信機能、いいね機能など)も追加できます
})
PostList.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import PostList from './PostList'
import { MemoryRouter } from 'react-router-dom'
// Mock the AWS Amplify API client
vi.mock('aws-amplify/api', () => ({
generateClient: vi.fn(() => ({
models: {
PostMessage: {
list: vi.fn(),
update: vi.fn(),
},
},
})),
}))
describe('PostList', () => {
it('renders post list', async () => {
const mockPosts = [
{ id: '1', userName: 'User1', message: 'Message 1', createdAt: '2023-01-01T00:00:00Z', good: 1 },
{ id: '2', userName: 'User2', message: 'Message 2', createdAt: '2023-01-02T00:00:00Z', good: 2 },
]
const client = (await import('aws-amplify/api')).generateClient()
client.models.PostMessage.list.mockResolvedValue({ data: mockPosts })
render(
<MemoryRouter>
<PostList />
</MemoryRouter>
)
expect(await screen.findByText('User1')).toBeDefined()
expect(screen.getByText('Message 1')).toBeDefined()
expect(screen.getByText('User2')).toBeDefined()
expect(screen.getByText('Message 2')).toBeDefined()
})
it('handles like button click', async () => {
const mockPosts = [
{ id: '1', userName: 'User1', message: 'Message 1', createdAt: '2023-01-01T00:00:00Z', good: 1 },
]
const client = (await import('aws-amplify/api')).generateClient()
client.models.PostMessage.list.mockResolvedValue({ data: mockPosts })
client.models.PostMessage.update.mockResolvedValue({ data: { ...mockPosts[0], good: 2 } })
render(
<MemoryRouter>
<PostList />
</MemoryRouter>
)
const likeButton = await screen.findByText('👍 1')
fireEvent.click(likeButton)
expect(await screen.findByText('&am
CreatePost.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import CreatePost from './CreatePost'
import { MemoryRouter } from 'react-router-dom'
// Mock the AWS Amplify API client
vi.mock('aws-amplify/api', () => ({
generateClient: vi.fn(() => ({
models: {
PostMessage: {
create: vi.fn(),
},
},
})),
}))
// Mock the useNavigate hook
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom')
return {
...actual,
useNavigate: () => vi.fn(),
}
})
describe('CreatePost', () => {
it('renders create post form', () => {
render(
<MemoryRouter>
<CreatePost />
</MemoryRouter>
)
expect(screen.getByText('新規投稿')).toBeDefined()
expect(screen.getByLabelText('ユーザー名')).toBeDefined()
expect(screen.getByLabelText('メッセージ')).toBeDefined()
expect(screen.getByText('投稿する')).toBeDefined()
})
it('handles form submission', async () => {
const client = (await import('aws-amplify/api')).generateClient()
client.models.PostMessage.create.mockResolvedValue({ data: {} })
render(
<MemoryRouter>
<CreatePost />
</MemoryRouter>
)
fireEvent.change(screen.getByLabelText('ユーザー名'), { target: { value: 'TestUser' } })
fireEvent.change(screen.getByLabelText('メッセージ'), { target: { value: 'Test message' } })
fireEvent.click(screen.getByText('投稿する'))
expect(client.models.PostMessage.create).toHaveBeenCalledWith({
message: 'Test message',
userName: 'TestUser',
good: 0,
})
})
// 他のテストケース(バリデーション、キャンセルボタンのクリックなど)も追加できます
})
これらのテストは、各コンポーネントの主要な機能をカバーしています。Vitestを使用してこれらのテストを実行するには、プロジェクトのルートディレクトリで以下のコマンドを実行します:
npm run test
または
yarn test
テストを正常に実行するためには、以下の追加の設定が必要かもしれません:
@testing-library/react と @testing-library/user-event をインストールする
Vitestの設定ファイル(vitest.config.ts)でReactのテスト環境を設定する
必要に応じて、モックやテストセットアップファイルを追加する
これらのテストは基本的な例であり、実際のアプリケーションの要件に合わせて調整が必要になる可能性があります。
実装した結果
全然動かないコードができた
2.画面を1つに絞ってテストコード作成を依頼
プロンプト
@CreatePost.tsx @Vitest このファイルに対して単体テストをVitestで書いてください。
import { describe, it, expect, vi } from 'vitest';
import { render, fireEvent } from '@testing-library/react';
import CreatePost from './CreatePost';
import { generateClient } from 'aws-amplify/api';
// モックの設定
vi.mock('aws-amplify/api', () => ({
generateClient: vi.fn(),
}));
vi.mock('react-router-dom', () => ({
useNavigate: () => vi.fn(),
}));
describe('CreatePost', () => {
it('renders correctly', () => {
const { getByText, getByLabelText } = render(<CreatePost />);
expect(getByText('新規投稿')).toBeTruthy();
expect(getByLabelText('ユーザー名')).toBeTruthy();
expect(getByLabelText('メッセージ')).toBeTruthy();
});
it('updates input values', () => {
const { getByLabelText } = render(<CreatePost />);
const userNameInput = getByLabelText('ユーザー名') as HTMLInputElement;
const messageInput = getByLabelText('メッセージ') as HTMLTextAreaElement;
fireEvent.change(userNameInput, { target: { value: 'TestUser' } });
fireEvent.change(messageInput, { target: { value: 'Test message' } });
expect(userNameInput.value).toBe('TestUser');
expect(messageInput.value).toBe('Test message');
});
it('submits form with valid data', async () => {
const mockCreate = vi.fn().mockResolvedValue({});
(generateClient as jest.Mock).mockReturnValue({
models: {
PostMessage: {
create: mockCreate,
},
},
});
const { getByLabelText, getByText } = render(<CreatePost />);
const userNameInput = getByLabelText('ユーザー名');
const messageInput = getByLabelText('メッセージ');
const submitButton = getByText('投稿する');
fireEvent.change(userNameInput, { target: { value: 'TestUser' } });
fireEvent.change(messageInput, { target: { value: 'Test message' } });
fireEvent.click(submitButton);
expect(mockCreate).toHaveBeenCalledWith({
message: 'Test message',
userName: 'TestUser',
good: 0,
});
});
});
実装した結果
精度は多少上がったが、まだまだ改善の余地がありそう
3.最初にテストケースを書いてもらい、テストケース毎にスコープ絞って実装
a.テストケースの出力
プロンプト
@CreatePost.tsx @Vitest 投稿画面のコードを確認し、テストコードをまずはitでテストケースだけ作成してください。テストケースの説明は日本語でお願いします。
はい、承知しました。@CreatePost.tsxの投稿画面のコードを確認し、Vitestを使用してテストケースを作成します。まずは、itブロックでテストケースの概要を日本語で記述します。
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import CreatePost from './CreatePost';
describe('CreatePost', () => {
it('投稿フォームが正しくレンダリングされること', () => {
// テストコードをここに実装
});
it('タイトルの入力フィールドに正しく入力できること', () => {
// テストコードをここに実装
});
it('本文の入力フィールドに正しく入力できること', () => {
// テストコードをここに実装
});
it('投稿ボタンをクリックしたときに、正しくonSubmit関数が呼び出されること', () => {
// テストコードをここに実装
});
it('タイトルが空の場合、エラーメッセージが表示されること', () => {
// テストコードをここに実装
});
it('本文が空の場合、エラーメッセージが表示されること', () => {
// テストコードをここに実装
});
it('投稿が成功した場合、成功メッセージが表示されること', () => {
// テストコードをここに実装
});
it('投稿が失敗した場合、エラーメッセージが表示されること', () => {
// テストコードをここに実装
});
});
これらのテストケースは、CreatePost コンポーネントの主要な機能と動作をカバーしています。各 it ブロック内に具体的なテストコードを実装することで、コンポーネントの挙動を確認することができます。
実際のテストコードを実装する際は、render 関数でコンポーネントをレンダリングし、screen や fireEvent などを使用して要素の存在確認やユーザーアクションのシミュレーションを行います。また、必要に応じてモックやスパイを使用して、外部の依存関係(例:API呼び出し)をテストから分離することも検討してください。
b.テストケースを指定してコードを出力する
プロンプト
@Vitest @CreatePost.tsx を見て、「投稿フォームが正しくレンダリングされること」のテストコード書いてください。
はい、承知しました。@CreatePost.tsx の内容を踏まえて、「投稿フォームが正しくレンダリングされること」のテストコードを Vitest を使って書きます。
以下は、想定される CreatePost コンポーネントの基本的な構造に基づいたテストコードです:
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import CreatePost from './CreatePost';
describe('CreatePost', () => {
it('投稿フォームが正しくレンダリングされること', () => {
render(<CreatePost />);
// タイトル入力フィールドが存在することを確認
expect(screen.getByLabelText('タイトル')).toBeInTheDocument();
// 本文入力フィールドが存在することを確認
expect(screen.getByLabelText('本文')).toBeInTheDocument();
// 投稿ボタンが存在することを確認
expect(screen.getByRole('button', { name: '投稿' })).toBeInTheDocument();
// フォームのヘッダーが正しく表示されていることを確認
expect(screen.getByText('新規投稿')).toBeInTheDocument();
});
});
このテストコードでは以下のことを確認しています:
タイトル入力フィールドが存在すること
本文入力フィールドが存在すること
投稿ボタンが存在すること
フォームのヘッダー("新規投稿"というテキスト)が表示されていること
注意点:
このテストコードは、CreatePost コンポーネントが特定の構造を持っていることを前提としています。実際のコンポーネントの実装に合わせて、セレクタやテキストを適宜調整する必要があります。
@testing-library/react の render 関数と screen オブジェクトを使用しています。これらをインポートしていることを確認してください。
Vitest で Jest の DOM 拡張を使用するには、設定ファイル(vite.config.ts や vitest.config.ts)で @testing-library/jest-dom をセットアップする必要があります。
このテストを実行することで、CreatePost コンポーネントが期待通りにレンダリングされ、必要な要素が全て存在することを確認できます。
実装してどうだったか
未実装のテストケースができるなど、手で修正する部分はあったがそれなりに動く
分かったこと
- スコープを絞ると精度が高くなった
- モックが鬼門
- ドキュメント参照だけでは動くテストコードができない
気をつけたいこと
- 一気にテストコードを作るのではなく、スコープを絞りひとつずつ作っていく方がよい
- AIにすべて任せるのではなく、AIと対話しながら作っていく必要がある
まとめ
3番目のやり方が一番筋がよさそうだと感じましたが、少なからず人間の手で修正する必要があり、AIにコーディングのすべてを任せることはできないことが分かりました。
今回試したChat形式のAIとコード補完のAI(Github copilotなど)の両方を使うことでもっと楽にコーディングできるのではないかという可能性も感じました。