# 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