EntitySetup
以宣告式語法準備測試資料
EntitySetup 讓你直接在 Gherkin 中宣告測試的前置資料,無需在 Step Definition 中撰寫 INSERT 語句。
基本用法
Given 準備一個使用者, with table:
| >Alice.id | name | email | status |
| <userId | Alice | alice@example.com | ACTIVE |這段指令會:
- 透過 Entity Spec 將「使用者」對應到
users資料表,並取得欄位定義 - 新增一筆
name = Alice、email = alice@example.com、status = ACTIVE的資料寫入資料庫 - 從資料庫反查
user_id欄位值,存入變數Alice.id
變數擷取
INSERT 後,可透過 > 和 < 配對從資料庫反查自動產生的欄位值(如 auto-increment ID),並存入變數供後續步驟使用:
>variableName(header row):定義變數名稱,用於儲存擷取的值<columnName(data row):指定要從資料庫反查的欄位名稱
Given 準備一個使用者, with table:
| >Alice.id | >Alice.createdAt | name | email | status |
| <userId | <createdAt | Alice | alice@example.com | ACTIVE |執行流程:
- 將
name、email、status寫入資料庫 - 從資料庫反查
userId欄位值 → 存入變數Alice.id - 從資料庫反查
createdAt欄位值 → 存入變數Alice.createdAt
引用變數
使用 $variable 引用先前儲存的變數:
Given 準備一個使用者, with table:
| >Alice.id | name |
| <userId | Alice |
Given 準備一個待辦事項, with table:
| userId | title |
| $Alice.id | 買牛奶 |JSON 欄位描述
當 Entity 欄位儲存 JSON 物件(如 PostgreSQL JSONB、MySQL JSON)時,提供兩種方式描述 JSON 欄位內容。
JSON Path DataTable Header
在 DataTable header 使用路徑語法展開 JSON 結構:
Given 準備一個訂單, with table:
| id | orderItems[0].productId | orderItems[0].quantity | orderItems[1].productId | orderItems[1].quantity |
| 1 | 5 | 2 | 6 | 10 |等同於壓縮為單行字串的寫法:
Given 準備一個訂單, with table:
| id | orderItems |
| 1 | [{"productId":5,"quantity":2},{"productId":6,"quantity":10}] |搭配 Symbol System:
Given 準備一個訂單, with table:
| id | orderItems[0].productId | orderItems[0].createdAt |
| 1 | $productId | @time(now) |JSON DocString 格式
使用 with json: 搭配 """json ... """ 直接撰寫結構化 JSON,適合深層巢狀結構:
Given 準備一個訂單, with json:
"""json
{
"id": 1,
"orderItems": [
{ "productId": 5, "quantity": 2 },
{ "productId": 6, "quantity": 10 }
]
}
"""多筆資料使用 JSON Array:
Given 準備一個訂單明細, with json:
"""json
[
{ "orderId": 1, "productId": 101, "quantity": 2 },
{ "orderId": 1, "productId": 102, "quantity": 1 }
]
"""搭配 Symbol System:
Given 準備一個訂單, with json:
"""json
{
>orderId: <id,
"orderNo": "ORD-001",
"createdAt": @time(now),
"orderItems": [
{ "productId": $productId, "quantity": 2 }
]
}
"""JSON 格式注意事項:符號系統的表達式(
$變數、&CAS 約束、@時間符號、>/<擷取鍵)不可放在 JSON 字串引號""內,否則會被視為純字串。
Key 含 . 的逃脫語法
當 JSON key 本身包含 . 字元時,使用 ["..."] bracket notation 避免被誤拆為多層巢狀:
Given 準備一個設定, with table:
| id | content.["btn.save"] | content.["btn.cancel"] |
| 1 | 儲存 | 取消 |JSON DocString 中直接使用原始 key,框架自動處理逃脫。
與 Entity Spec 的對應關係
EntitySetup 指令透過 Entity Spec 將業務實體名稱對應到實際的資料表,並根據 DDL 驗證欄位是否存在。
對應機制
# entity_to_table_mapping.yml
entity_to_table_mapping:
- 使用者: users
- 待辦事項: todos
- 訂單: orders
- 商品: products-- schema.sql
CREATE TABLE users (
user_id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
status VARCHAR(20) DEFAULT 'ACTIVE',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Given 準備一個使用者, with table:
| name | email |
| Alice | alice@example.com |
# SpecFormula 自動對應到 users 資料表
# INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')| Gherkin 元素 | 對應來源 |
|---|---|
使用者 | entity_to_table_mapping 的 key |
users 資料表 | entity_to_table_mapping 的 value |
name, email 欄位 | DDL 欄位定義 |
命名規則
Entity 名稱:使用業務團隊熟悉的中文或英文名稱
# ✅ 推薦:業務語言
entity_to_table_mapping:
- 會員: members
- 訂單: orders
- 商品: products
# ❌ 避免:技術名稱
entity_to_table_mapping:
- member_entity: members
- tbl_order: orders欄位名稱轉換:DataTable 使用 camelCase,自動對應資料庫的 snake_case
Gherkin: userId → DB: user_id
Gherkin: createdAt → DB: created_at在查詢或驗證資料庫欄位時,框架同樣會對每一個欄位名稱套用三層匹配邏輯:
- 第一層:精確匹配
- 先用原始欄位名稱直接找,例如
userId對userId。
- 先用原始欄位名稱直接找,例如
- 第二層:camelCase → snake_case
- 若找不到精確匹配,會將
userId轉成user_id,再嘗試匹配 DDL / 實際資料的欄位名稱。
- 若找不到精確匹配,會將
- 第三層:忽略大小寫
- 若前兩層都沒找到,最後會以「忽略大小寫」的方式比對,例如
user_id、USER_ID、User_Id都會被視為同一個欄位。
- 若前兩層都沒找到,最後會以「忽略大小寫」的方式比對,例如
| Feature 欄位名稱 | DDL / Map 欄位名稱 | 是否匹配 | 說明 |
|---|---|---|---|
userId | userId | ✅ 是 | 第一層:直接 equals 匹配 |
userId | user_id | ✅ 是 | 第二層:camelCase → snake_case |
userId | USER_ID | ✅ 是 | 第二層轉成 user_id 後,再第三層 equalsIgnoreCase 匹配 |
實務上只要在 Spec 中統一使用 camelCase,無論 DDL 是 snake_case 還是慣用全大寫命名,都能被穩定解析,避免在 SQL/程式碼裡手動處理大小寫與底線差異。
Schema 驗證
SpecFormula 會根據 DDL 驗證 DataTable 中的欄位是否存在:
# ✅ 正確:欄位存在於 DDL
Given 準備一個使用者, with table:
| name | email | status |
| Alice | alice@example.com | ACTIVE |
# ❌ Lint 錯誤:欄位不存在
Given 準備一個使用者, with table:
| name | emailAddress | # emailAddress 不存在,應為 email
| Alice | alice@example.com |為什麼不用 API 建立前置資料?
透過 API 建立前置資料會造成測試耦合——如果「建立使用者」API 有 bug,所有依賴該使用者的測試都會連帶失敗,即使被測功能完全正確。
# ❌ 用 API 建立:建立使用者 API 有 bug 會影響所有測試
When (No Actor) 建立使用者, call table:
| name | email |
| Alice | alice@example.com |
# ✅ EntitySetup:直接插入資料庫,不受 API 影響
Given 準備一個使用者, with table:
| name | email |
| Alice | alice@example.com |型別轉換
DataTable 中的值皆為字串,框架會根據 DDL 中對應欄位的型別定義(如 INT、DATE、DATETIME)自動將字串轉為正確的資料庫型別:
| DDL 欄位型別 | DataTable 輸入 | 轉換結果 |
|---|---|---|
INT / BIGINT | "123" | Integer / Long |
DECIMAL | "99.99" | BigDecimal |
DATE | "2026-01-27" | LocalDate |
DATETIME / TIMESTAMP | "2026-01-27T10:00:00" | LocalDateTime |
DATETIME / TIMESTAMP | @time("now") | 當前 Mock 時間 |
DATE | @date("now") | 當前 Mock 日期 |
錯誤處理
Lint 階段
| 錯誤 | 說明 |
|---|---|
| Entity 未定義 | entity_to_table_mapping 中找不到對應的 entity |
| 欄位不存在 | DataTable 中的欄位在資料表定義中找不到 |
執行階段
| 錯誤 | 說明 |
|---|---|
| INSERT 失敗 | 資料庫約束違反(唯一鍵衝突、NOT NULL 等) |
完整範例
Feature: 新增待辦事項
Background:
Given 現在時間為 "2026-01-27T10:00:00"
# EntitySetup 指令 — 準備使用者資料
# >Alice.id / <userId:INSERT 後反查 userId 存入 Alice.id
Given 準備一個使用者, with table:
| >Alice.id | name | email | passwordHash | status |
| <userId | Alice | alice@example.com | password123 | ACTIVE |
Example: 新增一筆待辦事項
# ApiCall 指令(參見 ApiCall 文件)
When (UID="$Alice.id") 新增待辦事項, call table:
| >todo1.id | title |
| <todoId | 買牛奶 |
# ResponseValidate 指令(參見 ResponseValidate 文件)
Then 新增待辦事項(201)回應, with table:
| todoId | userId | title |
| $todo1.id | $Alice.id | 買牛奶 |