測試步驟 - Clean Architecture in TypeScript
如果直接使用 Vitest 來撰寫測試,我們需要模擬大量的 API 請求和回應的檢查,這會造成單一測試非常不好閱讀,在維護新的功能時也需要反覆的產生大量重複的程式碼。
我們可以模仿 BDD(Behavior-Driven Development)風格的方式,設定「步驟」的概念,來讓測試案例變得容易理解,也能讓 AI 擴充測試時更加穩定。
步驟定義
一般來說要使用 BDD 風格的測試,通常會使用 Cucumber 這類框架,然而 Vitest 本身也是 Test Runner 的關係,要將兩者直接整合並不容易。
然而步驟定義(Step Definitions)並不一定要透過 Gherkin 語法來撰寫,我們可以自己定義一個函式來實現。
1export async function whenVisitHome() {
2 // ...
3}
基於這樣的方式,我們就可以將 Vitest 的測試案例整理為下方這樣的方式呈現
1describe("GET /", () => {
2 it("can access home page", async () => {
3 const res = await whenVisitHome()
4 await thenPageContains("Hello World")
5 })
6})
如果我們沒有明確說明這樣的實作風格,那麼 AI 就不會進行這樣的設計,即使 AI 是知道怎麼實現這樣的設計。
Cart API 重構
首先,我們先針對 Cart API 重構,因為情境比較單純。基於上一個階段 Mock Repository 的基礎,我們可以透過 Claude Code 使用以下指示完成重構。
這個步驟有可能因為 Mock Repository 的實作方式不同失敗,正常來說 storage 是 static 時 Repository 的資料就能順利被不同步驟使用。
1Create `test/steps/cart.ts` to create BDD-style steps for testing the cart functionality.
2
3```typescript
4export async function givenCartItems(cartId, items: { ... }[]): void {
5 // Resolve mock repository use tsyringe
6 // Save cart items to the mock repository
7}
8```
9
10When testing cart API, you should use the `givenCartItems` step to setup the initial state of the cart.
11
12```typescript
13import { givenCartItems } from './steps/cart';
14
15describe('Cart API', () => {
16 it('should return empty cart for new cart ID', async () => {
17 // Cart setup step
18 // Http step
19 // Assert step
20 });
21})
22```
23
24You do SHOULD NOT change any production code, use steps and mocks to test the cart API.
過程中 Claude Code 會經過幾次的處理,可以先使用 Plan Mode 確認修改範圍只限定在 test/ 目錄下,再繼續進行。
中間會請求單獨運行幾次測試,以及修正一些寫錯的地方。
最終成果會看到類似下面這樣的測試案例
1it('should return cart with items when items are added', async () => {
2 // Given
3 const cartId = 'test-cart-with-items';
4 await givenCartItems(cartId, [
5 { name: 'Apple', unitPrice: 100, quantity: 2 },
6 { name: 'Banana', unitPrice: 50, quantity: 3 }
7 ]);
8
9 // When
10 const response = await whenGetCart(cartId);
11
12 // Then
13 await thenCartShouldContain(response, [
14 { name: 'Apple', price: 100, quantity: 2 },
15 { name: 'Banana', price: 50, quantity: 3 }
16 ]);
17 });
在這樣的基礎,我們一定程度上可以確定 Cart API 呼叫後的回傳是我們預期的內容,在上一階段只做 GET 測試時,會因為無法放入測試資料而只檢查空值,相比之下這個版本的測試更加完整。
Chat API 重構
有了 Cart API 的經驗,要重構 Chat API 基本上只需要在給相同的指示即可,如果 Claude Code 的 Context 還足夠,接續使用會在更少的嘗試下完成,因為 Cart API 寫錯的地方會有一定程度的記憶,那麼 AI 在處理上就比較有機會避開。
1Create `test/steps/chat.ts` to create BDD-style steps for testing the cart functionality.
2
3```typescript
4export async function givenChatMessage(chatId, messages: { ... }[]): void {
5 // Resolve mock repository use tsyringe
6 // Save messages to the mock repository
7}
8```
9
10When testing chat API, you should use the `givenChatMessage` step to setup the initial state of the chat.
11
12```typescript
13import { givenChatMessage } from './steps/chat';
14
15describe('Chat API', () => {
16 it('should return empty messages for new chat ID', async () => {
17 // Chat setup step
18 // Http step
19 // Assert step
20 });
21})
22```
23
24You do SHOULD NOT change any production code, use steps and mocks to test the chat API. No need to test POST requests, only GET requests.
因為 Chat API 還包含了發送訊息的部分,所以需要將 POST 排除,我們會在後續的實作中單獨處理這個需要跟 AI 一起整合處理的情境。
確認我們可以得到像是這樣的測試案例,就表示已經成功
1it('should return conversation history in correct order', async () => {
2 const chatId = 'test-chat-conversation';
3
4 // Given
5 await givenChatMessage(chatId, [
6 { role: 'user', content: 'First message' },
7 { role: 'assistant', content: 'First response' },
8 { role: 'user', content: 'Second message' },
9 { role: 'assistant', content: 'Second response' }
10 ]);
11
12 // When
13 const response = await whenGetChatMessages(chatId);
14
15 // Then
16 await thenChatShouldContain(response, [
17 { role: 'user', content: 'First message' },
18 { role: 'assistant', content: 'First response' },
19 { role: 'user', content: 'Second message' },
20 { role: 'assistant', content: 'Second response' }
21 ]);
22 });
因為這是一個關鍵的樣板,我們最好讓 Claude Code 將這個行為記錄下來,所以會需要在 /clear 或者 /compact 只是之前,要求更新 CLAUDE.md 可以使用類似下方的指示處理
Update CLAUDE.md about how to use BDD-style test stpes
如果使用其他工具,那麼最好將上述產生的程式碼手動的寫到指示裡面,確保後續的測試都可以依照這個方式實作,就會相對穩定不少。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - 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
- 測試步驟 - Clean Architecture in TypeScript