---
title: "測試步驟 - Clean Architecture in TypeScript"
date: 2025-11-14T00:00:00+08:00
publishDate: 2025-11-14T00: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/14/clean-architecture-in-ts-test-steps/"
language: "zh-tw"
---



如果直接使用 Vitest 來撰寫測試，我們需要模擬大量的 API 請求和回應的檢查，這會造成單一測試非常不好閱讀，在維護新的功能時也需要反覆的產生大量重複的程式碼。

我們可以模仿 BDD（Behavior-Driven Development）風格的方式，設定「步驟」的概念，來讓測試案例變得容易理解，也能讓 AI 擴充測試時更加穩定。

<!--more-->

## 步驟定義{#step-definitions}

一般來說要使用 BDD 風格的測試，通常會使用 Cucumber 這類框架，然而 Vitest 本身也是 Test Runner 的關係，要將兩者直接整合並不容易。

然而步驟定義（Step Definitions）並不一定要透過 Gherkin 語法來撰寫，我們可以自己定義一個函式來實現。

```ts
export async function whenVisitHome() {
  // ...
}
```

基於這樣的方式，我們就可以將 Vitest 的測試案例整理為下方這樣的方式呈現

```ts
describe("GET /", () => {
  it("can access home page", async () => {
    const res = await whenVisitHome()
    await thenPageContains("Hello World")
  })
})
```

如果我們沒有明確說明這樣的實作風格，那麼 AI 就不會進行這樣的設計，即使 AI 是知道怎麼實現這樣的設計。

## Cart API 重構{#refactor-cart-api}

首先，我們先針對 Cart API 重構，因為情境比較單純。基於上一個階段 Mock Repository 的基礎，我們可以透過 Claude Code 使用以下指示完成重構。

> 這個步驟有可能因為 Mock Repository 的實作方式不同失敗，正常來說 storage 是 static 時 Repository 的資料就能順利被不同步驟使用。

````markdown
Create `test/steps/cart.ts` to create BDD-style steps for testing the cart functionality.

```typescript
export async function givenCartItems(cartId, items: { ... }[]): void {
  // Resolve mock repository use tsyringe
  // Save cart items to the mock repository
}
```

When testing cart API, you should use the `givenCartItems` step to setup the initial state of the cart.

```typescript
import { givenCartItems } from './steps/cart';

describe('Cart API', () => {
	it('should return empty cart for new cart ID', async () => {
	  // Cart setup step
	  // Http step
	  // Assert step
    });
})
```

You do SHOULD NOT change any production code, use steps and mocks to test the cart API.
````

過程中 Claude Code 會經過幾次的處理，可以先使用 Plan Mode 確認修改範圍只限定在 `test/` 目錄下，再繼續進行。

中間會請求單獨運行幾次測試，以及修正一些寫錯的地方。

最終成果會看到類似下面這樣的測試案例

```ts
it('should return cart with items when items are added', async () => {
    // Given
    const cartId = 'test-cart-with-items';
    await givenCartItems(cartId, [
      { name: 'Apple', unitPrice: 100, quantity: 2 },
      { name: 'Banana', unitPrice: 50, quantity: 3 }
    ]);
    
    // When
    const response = await whenGetCart(cartId);
    
    // Then
    await thenCartShouldContain(response, [
      { name: 'Apple', price: 100, quantity: 2 },
      { name: 'Banana', price: 50, quantity: 3 }
    ]);
  });
```

在這樣的基礎，我們一定程度上可以確定 Cart API 呼叫後的回傳是我們預期的內容，在上一階段只做 GET 測試時，會因為無法放入測試資料而只檢查空值，相比之下這個版本的測試更加完整。

## Chat API 重構{#refactor-chat-api}

有了 Cart API 的經驗，要重構 Chat API 基本上只需要在給相同的指示即可，如果 Claude Code 的 Context 還足夠，接續使用會在更少的嘗試下完成，因為 Cart API 寫錯的地方會有一定程度的記憶，那麼 AI 在處理上就比較有機會避開。

````markdown
Create `test/steps/chat.ts` to create BDD-style steps for testing the cart functionality.

```typescript
export async function givenChatMessage(chatId, messages: { ... }[]): void {
  // Resolve mock repository use tsyringe
  // Save messages to the mock repository
}
```

When testing chat API, you should use the `givenChatMessage` step to setup the initial state of the chat.

```typescript
import { givenChatMessage } from './steps/chat';

describe('Chat API', () => {
	it('should return empty messages for new chat ID', async () => {	
	  // Chat setup step
	  // Http step
	  // Assert step
    });
})
```

You 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 一起整合處理的情境。

確認我們可以得到像是這樣的測試案例，就表示已經成功

```ts
it('should return conversation history in correct order', async () => {
    const chatId = 'test-chat-conversation';
    
    // Given
    await givenChatMessage(chatId, [
      { role: 'user', content: 'First message' },
      { role: 'assistant', content: 'First response' },
      { role: 'user', content: 'Second message' },
      { role: 'assistant', content: 'Second response' }
    ]);
    
    // When
    const response = await whenGetChatMessages(chatId);
    
    // Then
    await thenChatShouldContain(response, [
      { role: 'user', content: 'First message' },
      { role: 'assistant', content: 'First response' },
      { role: 'user', content: 'Second message' },
      { role: 'assistant', content: 'Second response' }
    ]);
  });
```

因為這是一個關鍵的樣板，我們最好讓 Claude Code 將這個行為記錄下來，所以會需要在 `/clear` 或者 `/compact` 只是之前，要求更新 `CLAUDE.md` 可以使用類似下方的指示處理

```
Update CLAUDE.md about how to use BDD-style test stpes
```

如果使用其他工具，那麼最好將上述產生的程式碼手動的寫到指示裡面，確保後續的測試都可以依照這個方式實作，就會相對穩定不少。
