---
title: "測試準備 - Clean Architecture in TypeScript"
date: 2025-11-07T00:00:00+08:00
publishDate: 2025-11-07T00:00:00+08:00
lastmod: 2025-11-21T10:38:12+08:00
tags: ["TypeScript","Clean Architecture","架構","經驗","AI"]
series: "clean-architecture-in-ts"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/11/07/clean-architecture-in-ts-test-preparation/"
language: "zh-tw"
---



前面的內容我們都注重在功能的實現，然而使用 AI 開發使用測試來確保功能的穩定也是非常重要的，至少我們不會因為產生的程式碼有一些預期外的改動，而破壞整個功能的完整性。

<!--more-->

## 模型注入{#model-injection}

透過 AI 我們已經把依賴注入的部分處理完畢，然而在使用 AI 時還是直接在 `LlmAssistantService` 這個物件初始化 AI SDK 的物件，這對測試非常不友善，因此我們需要將模型也改為依賴注入的方式進行提供。

> AI SDK 也有提供測試工具，然而使用 Cloudflare 的 Vitest Pool 時，因為 Cloudflare Worker 不支援 `node:http` 會無法測試，但我們仍有辦法解決這個問題

接下來的修改 AI 還不太擅長，因此我們採用手動處理的方式速度會更快。

首先，修改 `src/container.ts` 登記 AI 模型。

```ts
import { createOpenAI, OpenAIProvider } from '@ai-sdk/openai';
import { type LanguageModel } from 'ai';

import { LlmAssistantService, LanguageModelToken } from '@/service/LlmAssistantService';
// ...

export const LanguageModelProviderToken = Symbol('LanguageModelProviderToken');

// Register Infrastructure dependencies
container.register<OpenAIProvider>(LanguageModelProviderToken, {
	useValue: createOpenAI({
		apiKey: env.OPENAI_API_KEY,
		baseURL: env.OPENAI_BASE_URL || 'https://api.openai.com/v1',
	})
});

container.register<LanguageModel>(LanguageModelToken, {
	useFactory: (container) => {
		const openai = container.resolve<OpenAIProvider>(LanguageModelProviderToken);
		return openai(env.OPENAI_MODEL);
	}
});
```

我們將原本 `LlmAssistantService` 中的 `createOpenAI` 和 `openai(env.OPENAI_MODEL)` 移動到 `src/container.ts` 中統一管理，因為 AI SDK 有提供 `LanguageModel` 的介面，也很好處理注入上的設定。

接下來更新 `src/service/LlmAssistantService.ts` 的部分，將原本使用 `openai(env.OPENAI_MODEL)` 相關實作，改為使用依賴注入的 `this.model` 進行替換。

```ts
@injectable()
export class LlmAssistantService implements AssistantService {
  constructor(
  	@inject(LanguageModelToken) private readonly model: LanguageModel,
    @inject(CartRepositoryToken) private readonly cartRepository: CartRepository,
    @inject(ProductRepositoryToken) private readonly productRepository: ProductRepository
  ) {}

  async ask(chatId: string, messages: ChatMessage[]): Promise<string> {
    // Convert our ChatMessage entities to the format expected by AI SDK
    const aiMessages = messages.map(msg => ({
      role: msg.role as 'user' | 'assistant',
      content: msg.content
    }));

	const { text } = await generateText({
      model: this.model,
      // ...
    )}
  }
}
```

這樣一來，我們在測試環境就可以抽換 `LanguageModel` 為我們想要的物件，也不會受到 AI SDK 的 `node:http` 相容性影響。

## Vitest 安裝{#setup-vitest}

我們初期是使用 `create-cloudflare` 來初始化專案的，理論上 Vitest 已經會設定好，不過我們會需要針對像是 `tsconfig.json` 的 Path Alias 做一些處理，另一方面還可以讓 AI 幫我們先做一些簡單的初始化準備。

開始之前，如果已經安裝過 `vitest` 可以先修改 `package.json`

```json
{
  // ...
  "test": "vitest --run"
}
```

把 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 來產生初步的實作。

```markdown
Let's setup a testing environment:

1. Update vitest config to use `test/setup.ts`, config mock repositories, use `beforeEach` to reset the database before each test.
   - Use `test/setup.ts` to configure the mock repositories.
   - Use `beforeEach` to reset the database state before each test.
2. ONLY add mock repositories, MUST implementation in the `test/support`
3. Test Cart / Chat api with mock repositories, SHOULD be `test/cart.spec.ts` and `test/chat.spec.ts`

DO NOT add any test related to creating method, only GET methods.
MUST use intergration tests for Cloudflare workers, you can reference the `index.spec.ts` file.
```

上述的指示已經修過好幾個版本，是相對穩定的做法。

相比軟體的實作，目前還有許多專案沒有設定好測試，或者正確的對測試設定，因此 AI 也有很高的機率使用錯誤的方式，此時就需要給更明確的指示。

> 實際上產生出來的版本還是差強人意，而且根本是「有測試就好」的狀態

這邊可以先這樣處理，主要是節省我們進行初步的測試環境搭建為主，後續我們會一點一點修正這些問題，變成可行的測試狀態，也讓後續 AI 在實作時能參考這些做法。
