deepmerge-plus
Version:
用於深度(遞迴)合併 JavaScript 物件的函式庫 / A library for deep (recursive) merging of JavaScript objects
292 lines (224 loc) • 7.59 kB
Markdown
# 快照測試慣例 / Snapshot Testing Convention
## 0. 與傳統斷言的比較
| 面向 | 傳統斷言 | 文件化快照 |
|------|---------|-----------|
| 主要目標 | 驗證正確性 | 行為展示 + 驗證 |
| 可讀性 | 取決於測試名稱 | 快照本身即文件 |
| 維護成本 | 低 | 中(需更新說明) |
| 適用場景 | 單元測試 | API 範例、教學 |
## 1. 快照物件結構
```typescript
expect({
explain: '...', // 測試說明(必填)
tags?: 'tag1, tag2, ...', // 分類標籤(可選,有特別意義或需檢索時使用)
result: { ... }, // 輸出結果(必填)
input?: { // 輸入資料(可選,簡單時保留)
target: { ... },
source: { ... },
},
}).toMatchSnapshot()
```
### 必填欄位
| 欄位 | 類型 | 說明 |
|------|------|------|
| `explain` | `string` | 測試說明(必填,作為標題/描述/邏輯解釋,單行或多行皆可) |
| `result` | `object` | 函式輸出結果(必填,始終保留) |
### 可選欄位
| 欄位 | 類型 | 說明 |
|------|------|------|
| `tags` | `string` | 分類標籤(以 `, ` 分隔),只有當測試項目有特別意義或需要被檢索時才需要加上 |
| `input` | `object` | 輸入資料(來自函式參數,簡單時保留) |
## 2. 欄位規範
### 2.1 explain - 測試說明
- **必填**:每個快照至少需要 `explain`
- **用途**:幫助快速理解快照的內容與邏輯
- **類型**:
- **標題式**:簡短說明測試目的
- **描述式**:描述預期行為
- **邏輯式**:解釋合併邏輯
- **複合式**:結合以上,以快速理解為主
- **格式**:雙語(繁中 + 英文)或純英文
- **長度**:可以是**單行**或**多行**文字,視說明需求而定
- **擴展用途**:可以補充說明 `result` 中各欄位的來源或意義,幫助理解輸出結構
```typescript
// 短句範例
explain: '✅ 新增鍵 / Add keys'
// 複雜範例:說明 result 中各欄位的來源
explain: '✅ 多個物件合併(不同類型):所有屬性被合併
result 中
- first: 來自第一個物件
- second: 來自第二個物件
- third: 來自第三個物件
- fourth: 來自第四個物件'
```
### 2.2 tags - 分類標籤
- **格式**:以 `, ` (逗號 + 空格)分隔的多重標籤
- **語法**:字串形式 `tag1, tag2, tag3`(非陣列)
- **用途**:人類可讀的分類識別
- **使用時機**:只有當測試項目有特別意義或需要被檢索時,才需要加上 `tags`
```typescript
// ✅ 正確
tags: 'basic, root-level, keys'
// ❌ 錯誤(陣列形式)
tags: ['basic', 'root-level']
```
### 2.3 input - 輸入資料
- **來源**:來自函式參數,不一定是 `target` / `source`
- **結構**:根據函式簽章決定(如 `{ array: [...] }`、`{ options: {} }`)
- **判定邏輯**:
```
input 複雜度判定
│
▼
屬性數量 ≤ 3 且 無深度嵌套?
│
├─ 是 → 保留 input
│
└─ 否 → 移除 input(依賴 result 推導)
```
```typescript
// ✅ 簡單案例:保留 input(來自函式參數)
input: { array: [1, 2, 3] }
input: { target: { a: 1 }, source: { b: 2 } }
// ✅ 複雜案例:移除 input
// (依賴快照差異顯示輸入輸出變化)
```
### 2.4 result - 輸出結果
- **必填**:始終保留
- **用途**:斷言驗證 + 行為展示
## 3. tags 命名慣例
### 3.1 常用標籤分類
| 類別 | 標籤範例 | 說明 |
|------|---------|------|
| **測試類型** | `basic`, `advanced`, `edge-case` | 測試複雜度 |
| **資料結構** | `object`, `array`, `nested`, `deep` | 輸入資料類型 |
| **功能特性** | `clone`, `merge`, `replace` | 合併行為 |
| **應用場景** | `config`, `preferences`, `state` | 實際應用 |
| **對照組** | `good-example`, `bad-example` | 展示正確/錯誤用法 |
### 3.2 命名格式
- 使用 **小寫字母**
- 使用 `-` 或 `_` 分隔詞彙:`nested-object`, `deep_merge`
- 避免中文標籤
```typescript
// ✅ 正確
tags: 'basic, nested-object, merge'
// ❌ 錯誤
tags: '基礎, 巢狀, 合併'
```
## 4. 範例
### 4.1 簡單案例
```typescript
it('should add keys to empty target', () => {
const target = {};
const source = { key1: 'value1', key2: 'value2' };
const result = merge(target, source);
expect({
explain: '✅ 新增至空物件 / Add to empty object',
tags: 'basic, root-level, keys',
input: { target, source }, // 來自函式參數
result,
}).toMatchSnapshot();
});
```
### 4.2 deepmergeAll 案例
```typescript
it('should merge array of objects', () => {
const input = [{ a: 1 }, { b: 2 }];
const result = deepmergeAll(input);
expect({
explain: '合併物件陣列',
input: { input }, // 參數名稱為 input
result,
}).toMatchSnapshot();
});
```
### 4.2 複雜案例
```typescript
it('should deeply merge nested objects', () => {
const target = {
user: {
name: 'Alice',
settings: { theme: 'dark' }
}
};
const source = {
user: {
age: 30,
settings: { language: 'en' }
}
};
const result = merge(target, source);
expect({
explain: 'Deep nested merge preserves nested structure',
tags: 'nested, deep, object-merge',
result,
}).toMatchSnapshot();
});
```
### 4.3 比較測試(對照組)
當需要比較 deepmerge 與其他函式庫(如 lodash)的行為差異時使用。
```typescript
it('should match lodash _.assign behavior', () => {
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
// deepmerge 結果
const resultDeepmerge = deepmerge(target, source);
// lodash 結果
const resultLodash = _.assign({}, target, source);
expect({
explain: `deepmerge 模擬 _.assign:
- target: { a: 1, b: 2 }
- source: { b: 3, c: 4 }
- 結果:b 被 source 覆蓋`,
result: {
resultDeepmerge,
resultLodash,
isEqual: resultDeepmerge.a === resultLodash.a &&
resultDeepmerge.b === resultLodash.b &&
resultDeepmerge.c === resultLodash.c
},
}).toMatchSnapshot();
});
```
**快照結構**:
```json
{
"explain": "deepmerge 模擬 _.assign:...",
"result": {
"resultDeepmerge": { "a": 1, "b": 3, "c": 4 },
"resultLodash": { "a": 1, "b": 3, "c": 4 },
"isEqual": true
}
}
```
**說明**:
- 使用 `resultDeepmerge` 和 `resultLodash` 兩個鍵名(而非 `deepmergeResult` / `lodashResult`)
- 最後一個鍵為 `isEqual` 用於快速識別結果是否相同
- explain 使用模板字串(反引號)進行多行說明
## 5. 相關資源
- [Jest Snapshot Testing](https://jestjs.io/docs/snapshot-testing)
- [deepmerge 測試檔案](./test/)
## 6. 核心規範速查
### 最小結構
```typescript
expect({
explain: '說明 / 描述 / 邏輯解釋', // 必填(標題/描述/邏輯/複合式)
result: { /* ... */ }, // 必填(始終保留)
}).toMatchSnapshot()
```
### 完整結構
```typescript
expect({
explain: '說明 / Description', // 必填
tags?: 'tag1, tag2, tag3', // 可選
result: { /* ... */ }, // 必填
input?: { target, source }, // 可選
}).toMatchSnapshot()
```