測試準備 - Clean Architecture in TypeScript
前面的內容我們都注重在功能的實現,然而使用 AI 開發使用測試來確保功能的穩定也是非常重要的,至少我們不會因為產生的程式碼有一些預期外的改動,而破壞整個功能的完整性。
模型注入
透過 AI 我們已經把依賴注入的部分處理完畢,然而在使用 AI 時還是直接在 LlmAssistantService 這個物件初始化 AI SDK 的物件,這對測試非常不友善,因此我們需要將模型也改為依賴注入的方式進行提供。
AI SDK 也有提供測試工具,然而使用 Cloudflare 的 Vitest Pool 時,因為 Cloudflare Worker 不支援
node:http會無法測試,但我們仍有辦法解決這個問題
接下來的修改 AI 還不太擅長,因此我們採用手動處理的方式速度會更快。
首先,修改 src/container.ts 登記 AI 模型。
1import { createOpenAI, OpenAIProvider } from '@ai-sdk/openai';
2import { type LanguageModel } from 'ai';
3
4import { LlmAssistantService, LanguageModelToken } from '@/service/LlmAssistantService';
5// ...
6
7export const LanguageModelProviderToken = Symbol('LanguageModelProviderToken');
8
9// Register Infrastructure dependencies
10container.register<OpenAIProvider>(LanguageModelProviderToken, {
11 useValue: createOpenAI({
12 apiKey: env.OPENAI_API_KEY,
13 baseURL: env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
14 })
15});
16
17container.register<LanguageModel>(LanguageModelToken, {
18 useFactory: (container) => {
19 const openai = container.resolve<OpenAIProvider>(LanguageModelProviderToken);
20 return openai(env.OPENAI_MODEL);
21 }
22});
我們將原本 LlmAssistantService 中的 createOpenAI 和 openai(env.OPENAI_MODEL) 移動到 src/container.ts 中統一管理,因為 AI SDK 有提供 LanguageModel 的介面,也很好處理注入上的設定。
接下來更新 src/service/LlmAssistantService.ts 的部分,將原本使用 openai(env.OPENAI_MODEL) 相關實作,改為使用依賴注入的 this.model 進行替換。
1@injectable()
2export class LlmAssistantService implements AssistantService {
3 constructor(
4 @inject(LanguageModelToken) private readonly model: LanguageModel,
5 @inject(CartRepositoryToken) private readonly cartRepository: CartRepository,
6 @inject(ProductRepositoryToken) private readonly productRepository: ProductRepository
7 ) {}
8
9 async ask(chatId: string, messages: ChatMessage[]): Promise<string> {
10 // Convert our ChatMessage entities to the format expected by AI SDK
11 const aiMessages = messages.map(msg => ({
12 role: msg.role as 'user' | 'assistant',
13 content: msg.content
14 }));
15
16 const { text } = await generateText({
17 model: this.model,
18 // ...
19 )}
20 }
21}
這樣一來,我們在測試環境就可以抽換 LanguageModel 為我們想要的物件,也不會受到 AI SDK 的 node:http 相容性影響。
Vitest 安裝
我們初期是使用 create-cloudflare 來初始化專案的,理論上 Vitest 已經會設定好,不過我們會需要針對像是 tsconfig.json 的 Path Alias 做一些處理,另一方面還可以讓 AI 幫我們先做一些簡單的初始化準備。
開始之前,如果已經安裝過 vitest 可以先修改 package.json
1{
2 // ...
3 "test": "vitest --run"
4}
把 Vitest 預設改為 --run 模式,這樣才不會在使用 Claude Code 或者其他工具時,因為 Watch Mode 卡住,而是讓 AI 自動依照工具的輸出繼續調整。
我們可以對 Claude Code 下達這樣的指示:
Update vitest.config.mts to add path alias, and update `test/index.spec.ts` to use integration tests to verify the index is available.
基本上就會自動把 tsconfig.json 設定過的 Path Alias 問題解決,並且將原本 Hello World 的測試改為檢查首頁的 HTML 是否有正常被渲染出來。
這樣我們就初步準備好測試的環境,可以繼續進一步的設定。
Mock Repository
我們可以在測試環境加入 Mock Repository 來代替真實的 Repository 行為,雖然使用 Cloudflare 的 Vitest Pool Worker 也能夠模擬 KV 環境,但是要產生測試資料跟重設還是相對不方便,這個階段可以嘗試用 Claude Code 來產生初步的實作。
1Let's setup a testing environment:
2
31. Update vitest config to use `test/setup.ts`, config mock repositories, use `beforeEach` to reset the database before each test.
4 - Use `test/setup.ts` to configure the mock repositories.
5 - Use `beforeEach` to reset the database state before each test.
62. ONLY add mock repositories, MUST implementation in the `test/support`
73. Test Cart / Chat api with mock repositories, SHOULD be `test/cart.spec.ts` and `test/chat.spec.ts`
8
9DO NOT add any test related to creating method, only GET methods.
10MUST use intergration tests for Cloudflare workers, you can reference the `index.spec.ts` file.
上述的指示已經修過好幾個版本,是相對穩定的做法。
相比軟體的實作,目前還有許多專案沒有設定好測試,或者正確的對測試設定,因此 AI 也有很高的機率使用錯誤的方式,此時就需要給更明確的指示。
實際上產生出來的版本還是差強人意,而且根本是「有測試就好」的狀態
這邊可以先這樣處理,主要是節省我們進行初步的測試環境搭建為主,後續我們會一點一點修正這些問題,變成可行的測試狀態,也讓後續 AI 在實作時能參考這些做法。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in TypeScript
- 目標設定 - Clean Architecture in TypeScript
- Hono 框架 - Clean Architecture in TypeScript
- tsyringe 套件 - Clean Architecture in TypeScript
- 專案設定 - Clean Architecture in TypeScript
- 介面規劃 - Clean Architecture in TypeScript
- 架構規劃 - Clean Architecture in TypeScript
- 助手對話介面 - Clean Architecture in TypeScript
- 對話紀錄 API - Clean Architecture in TypeScript
- 對話紀錄 UseCase - Clean Architecture in TypeScript
- 整合大型語言模型 - Clean Architecture in TypeScript
- 對話 UseCase - Clean Architecture in TypeScript
- 購物車側欄 - Clean Architecture in TypeScript
- 側欄 Use Case - Clean Architecture in TypeScript
- 查詢商品 - Clean Architecture in TypeScript
- 更新購物車 - Clean Architecture in TypeScript
- 對話階段 - Clean Architecture in TypeScript
- 依賴注入 - Clean Architecture in TypeScript
- 測試準備 - Clean Architecture in TypeScript