# Shop Service 单元测试总结

**测试文件**: `tests/unit/shop_service_test.go`
**完成时间**: 2026-01-09
**状态**: ✅ 全部通过

---

## 一、测试概述

本次为 Shop Service（店铺业务服务层）编写了完整的单元测试，重点验证了层级校验逻辑、业务规则验证和错误处理。

---

## 二、测试覆盖

### 2.1 TestShopService_Create（创建店铺）

**测试用例数**: 6 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 创建一级店铺成功 | 验证创建一级店铺的基本流程 | ✅ |
| 创建二级店铺成功 | 验证创建下级店铺并正确计算层级 | ✅ |
| 层级校验-创建第8级店铺应失败 | **核心测试**：验证最大层级限制（7级） | ✅ |
| 店铺编号唯一性检查-重复编号应失败 | 验证店铺编号唯一性约束 | ✅ |
| 上级店铺不存在应失败 | 验证上级店铺存在性检查 | ✅ |
| 未授权访问应失败 | 验证用户授权检查 | ✅ |

**核心测试详解**：
```go
// 创建 7 级店铺层级结构
for i := 1; i <= 7; i++ {
    var parentID *uint
    if i > 1 {
        parentID = &shops[i-2].ID
    }
    // 创建第 i 级店铺
    shopModel := &model.Shop{
        Level:    i,
        ParentID: parentID,
        // ...
    }
    err := shopStore.Create(ctx, shopModel)
    require.NoError(t, err)
}

// 尝试创建第 8 级店铺（应该失败）
req := &model.CreateShopRequest{
    ParentID: &shops[6].ID, // 第7级店铺的ID
    // ...
}
result, err := service.Create(ctx, req)
assert.Error(t, err)

// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeShopLevelExceeded, appErr.Code)
assert.Contains(t, appErr.Message, "不能超过 7 级")
```

### 2.2 TestShopService_Update（更新店铺）

**测试用例数**: 4 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 更新店铺信息成功 | 验证更新店铺基本信息 | ✅ |
| 更新店铺编号-唯一性检查 | 验证更新时的编号唯一性 | ✅ |
| 更新不存在的店铺应失败 | 验证店铺存在性检查 | ✅ |
| 未授权访问应失败 | 验证用户授权检查 | ✅ |

### 2.3 TestShopService_Disable（禁用店铺）

**测试用例数**: 3 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 禁用店铺成功 | 验证禁用功能并检查状态变更 | ✅ |
| 禁用不存在的店铺应失败 | 验证店铺存在性检查 | ✅ |
| 未授权访问应失败 | 验证用户授权检查 | ✅ |

### 2.4 TestShopService_Enable（启用店铺）

**测试用例数**: 3 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 启用店铺成功 | 验证启用功能并检查状态变更 | ✅ |
| 启用不存在的店铺应失败 | 验证店铺存在性检查 | ✅ |
| 未授权访问应失败 | 验证用户授权检查 | ✅ |

**注意事项**：
- GORM 在保存时会忽略零值（`Status=0`），导致使用数据库默认值
- 测试中先创建启用状态的店铺，再通过 Update 禁用，最后测试 Enable 功能

### 2.5 TestShopService_GetByID（获取店铺详情）

**测试用例数**: 2 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 获取存在的店铺 | 验证正常查询流程 | ✅ |
| 获取不存在的店铺应失败 | 验证错误处理 | ✅ |

### 2.6 TestShopService_List（查询店铺列表）

**测试用例数**: 1 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 查询店铺列表 | 验证列表查询功能 | ✅ |

### 2.7 TestShopService_GetSubordinateShopIDs（获取下级店铺ID列表）

**测试用例数**: 1 个
**全部通过**: ✅

| 测试用例 | 目的 | 状态 |
|---------|------|------|
| 获取下级店铺 ID 列表 | 验证递归查询功能 | ✅ |

---

## 三、测试结果

```
=== RUN   TestShopService_Create
--- PASS: TestShopService_Create (11.25s)
    --- PASS: TestShopService_Create/创建一级店铺成功 (0.16s)
    --- PASS: TestShopService_Create/创建二级店铺成功 (0.29s)
    --- PASS: TestShopService_Create/层级校验-创建第8级店铺应失败 (0.59s)
    --- PASS: TestShopService_Create/店铺编号唯一性检查-重复编号应失败 (0.14s)
    --- PASS: TestShopService_Create/上级店铺不存在应失败 (0.07s)
    --- PASS: TestShopService_Create/未授权访问应失败 (0.00s)

=== RUN   TestShopService_Update
--- PASS: TestShopService_Update (7.86s)
    --- PASS: TestShopService_Update/更新店铺信息成功 (0.22s)
    --- PASS: TestShopService_Update/更新店铺编号-唯一性检查 (0.16s)
    --- PASS: TestShopService_Update/更新不存在的店铺应失败 (0.02s)
    --- PASS: TestShopService_Update/未授权访问应失败 (0.00s)

=== RUN   TestShopService_Disable
--- PASS: TestShopService_Disable (8.01s)
    --- PASS: TestShopService_Disable/禁用店铺成功 (0.29s)
    --- PASS: TestShopService_Disable/禁用不存在的店铺应失败 (0.02s)
    --- PASS: TestShopService_Disable/未授权访问应失败 (0.00s)

=== RUN   TestShopService_Enable
--- PASS: TestShopService_Enable (9.29s)
    --- PASS: TestShopService_Enable/启用店铺成功 (0.49s)
    --- PASS: TestShopService_Enable/启用不存在的店铺应失败 (0.03s)
    --- PASS: TestShopService_Enable/未授权访问应失败 (0.00s)

=== RUN   TestShopService_GetByID
--- PASS: TestShopService_GetByID (9.27s)
    --- PASS: TestShopService_GetByID/获取存在的店铺 (0.18s)
    --- PASS: TestShopService_GetByID/获取不存在的店铺应失败 (0.04s)

=== RUN   TestShopService_List
--- PASS: TestShopService_List (9.24s)
    --- PASS: TestShopService_List/查询店铺列表 (0.45s)

=== RUN   TestShopService_GetSubordinateShopIDs
--- PASS: TestShopService_GetSubordinateShopIDs (8.98s)
    --- PASS: TestShopService_GetSubordinateShopIDs/获取下级店铺_ID_列表 (0.40s)

PASS
ok  	command-line-arguments	64.887s
```

**总计**: 20 个测试用例全部通过 ✅

---

## 四、测试要点

### 4.1 Context 用户 ID 模拟

Service 层需要从 Context 中获取当前用户 ID，测试中使用辅助函数模拟：

```go
// createContextWithUserID 创建带用户 ID 的 context
func createContextWithUserID(userID uint) context.Context {
    return context.WithValue(context.Background(), constants.ContextKeyUserID, userID)
}
```

### 4.2 层级校验测试策略

**7 级层级创建**：
1. 循环创建 1-7 级店铺，每级店铺的 `parent_id` 指向上一级
2. 验证第 7 级店铺创建成功
3. 尝试创建第 8 级店铺，验证返回 `CodeShopLevelExceeded` 错误

**关键代码**：
```go
// 计算新店铺的层级
level = parent.Level + 1

// 校验层级不超过最大值
if level > constants.MaxShopLevel {
    return nil, errors.New(errors.CodeShopLevelExceeded, "店铺层级不能超过 7 级")
}
```

### 4.3 唯一性约束测试

**店铺编号唯一性**：
1. 创建第一个店铺（编号 `CODE_001`）
2. 尝试创建第二个相同编号的店铺
3. 验证返回 `CodeShopCodeExists` 错误

**更新时唯一性检查**：
1. 创建两个不同编号的店铺（`CODE_001`、`CODE_002`）
2. 尝试将 `CODE_002` 更新为 `CODE_001`
3. 验证返回 `CodeShopCodeExists` 错误

### 4.4 授权检查测试

所有需要授权的方法都测试了未授权访问场景：
- Create
- Update
- Disable
- Enable

使用不带用户 ID 的 `context.Background()` 模拟未授权访问，验证返回 `CodeUnauthorized` 错误。

### 4.5 错误码验证

所有错误测试都验证了具体的错误码：

```go
// 验证错误码
appErr, ok := err.(*errors.AppError)
require.True(t, ok, "错误应该是 AppError 类型")
assert.Equal(t, errors.CodeShopLevelExceeded, appErr.Code)
assert.Contains(t, appErr.Message, "不能超过 7 级")
```

---

## 五、测试覆盖的业务逻辑

### 5.1 Create 方法

✅ 用户授权检查
✅ 店铺编号唯一性检查
✅ 上级店铺存在性验证
✅ 层级计算（`level = parent.Level + 1`）
✅ **层级校验（最多 7 级）**
✅ 默认状态设置（`StatusEnabled`）
✅ Creator/Updater 字段设置

### 5.2 Update 方法

✅ 用户授权检查
✅ 店铺存在性验证
✅ 店铺编号唯一性检查（如果修改了编号）
✅ 部分字段更新（使用指针判断是否更新）
✅ Updater 字段更新

### 5.3 Disable/Enable 方法

✅ 用户授权检查
✅ 店铺存在性验证
✅ 状态更新
✅ Updater 字段更新

### 5.4 GetByID 方法

✅ 店铺存在性验证
✅ 错误处理（店铺不存在）

### 5.5 List 方法

✅ 列表查询功能
✅ 分页支持

### 5.6 GetSubordinateShopIDs 方法

✅ 递归查询下级店铺
✅ 包含自己（用于数据权限过滤）

---

## 六、测试技巧和最佳实践

### 6.1 Table-Driven Tests

虽然本次测试主要使用 `t.Run()` 子测试，但在 Create 测试中展示了适合多用例的场景。

### 6.2 辅助函数

```go
func createContextWithUserID(userID uint) context.Context {
    return context.WithValue(context.Background(), constants.ContextKeyUserID, userID)
}
```

封装常用操作，提高测试代码的可读性和可维护性。

### 6.3 错误验证模式

```go
// 1. 验证有错误
assert.Error(t, err)
assert.Nil(t, result)

// 2. 验证错误类型
appErr, ok := err.(*errors.AppError)
require.True(t, ok)

// 3. 验证错误码
assert.Equal(t, errors.CodeXxx, appErr.Code)

// 4. 验证错误消息
assert.Contains(t, appErr.Message, "关键词")
```

### 6.4 数据准备和清理

使用 `testutils.SetupTestDB()` 和 `defer testutils.TeardownTestDB()` 确保测试隔离：

```go
db, redisClient := testutils.SetupTestDB(t)
defer testutils.TeardownTestDB(t, db, redisClient)
```

---

## 七、遗留问题和改进建议

### 7.1 已解决的问题

**问题**: GORM 零值处理
**现象**: 创建 `Status=0`（StatusDisabled）的店铺时，GORM 忽略零值，使用数据库默认值 1
**解决**: 先创建启用状态的店铺，再通过 Update 禁用

**改进建议**: 在 Shop model 中使用 `*int` 指针类型存储 Status，或使用 `gorm:"default:0"` 显式指定默认值

### 7.2 未来优化方向

1. **性能测试**: 测试 7 级递归查询的性能
2. **并发测试**: 测试并发创建相同编号的店铺
3. **集成测试**: 测试 Service 层与 Handler 层的集成
4. **边界测试**: 测试极端场景（如超长字符串、特殊字符）

---

## 八、总结

### 完成度

✅ **100% 完成** - 所有计划的测试用例都已实现并通过

### 测试质量

- ✅ 覆盖所有公开方法
- ✅ 重点测试核心业务逻辑（层级校验）
- ✅ 完整的错误处理验证
- ✅ 授权检查覆盖
- ✅ 边界条件测试

### 核心成果

**最重要的测试**：**层级校验测试**（创建第8级店铺应失败）

这个测试验证了系统的核心业务规则：
- 店铺层级最多 7 级
- 超过限制时正确返回错误码
- 错误消息清晰明确

这确保了系统在生产环境中不会出现超过 7 级的店铺层级，符合业务需求。

---

**测试完成时间**: 2026-01-09
**测试通过率**: 100% (20/20)
**总耗时**: 64.887s

