应依赖并mock redis.Cmdable 接口而非 *redis.Client;用 mockery 生成 Cmdable mock,或使用 redis.MemoryClient 快速验证缓存逻辑,注意正确返回 redis.Nil 和 context 超时行为。

用 gomock 或 mockery 替换 redis.Client 接口
Gin 本身不耦合 Redis,真正要 mock 的是你自己封装的缓存层(比如一个实现了 CacheService 接口的结构体)。Redis 官方客户端 github.com/go-redis/redis/v9 的 *redis.Client 是具体类型,不能直接 mock;但它的核心方法都来自 redis.Cmdable 接口 —— 这才是你应该依赖和 mock 的目标。
实操建议:
- 把业务代码中对
*redis.Client的直接依赖,改为接收redis.Cmdable(或你自定义的更窄接口,如CacheReader/CacheWriter) - 用
mockery自动生成Cmdable的 mock 实现:mockery --name=Cmdable --output=./mocks - 测试时注入 mock 实例,例如:
service := NewUserService(mockRedis) - 避免 mock 整个
*redis.Client:它包含大量未使用的方法,mock 规则臃肿且易断
redis.MemoryClient 可以用,但仅限简单场景
github.com/bsm/redislock/v2 生态里有个轻量级内存实现 redis.MemoryClient,它实现了 redis.Cmdable,能跑通 SET/GET/DEL 等基础命令。但它不支持 Lua 脚本、Pipeline、Stream、Pub/Sub 等特性,也**不校验 key 类型**(比如对一个 string key 执行 HGET 不报错)。
适用场景:
- 只测缓存读写逻辑,不涉及原子操作或复杂数据结构
- 想快速验证 HTTP handler 中缓存命中/未命中的分支路径
- CI 环境不想起真实 Redis 容器(但要注意:它不是线程安全的,测试并发需加
t.Parallel()隔离)
示例初始化:mockRedis := redis.NewMemoryClient(),然后传给你的服务构造函数即可。
测试带 context.WithTimeout 的 Redis 调用要主动 cancel
生产代码里 Redis 方法几乎都带 context.Context 参数,比如 client.Get(ctx, key)。如果测试中用 context.Background() 或未设超时,mock 或 memory client 不会触发 timeout 行为,导致你误以为“超时逻辑没被覆盖”。
正确做法:
- 测试 timeout 分支时,显式创建带 deadline 的 ctx:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) - 在 defer 中调用
cancel(),防止 goroutine 泄漏 - 若用 mock,可在 mock 方法里 sleep 超过 deadline 时间,再返回
context.DeadlineExceeded - 别依赖 “mock 返回 error 就等于超时”——错误类型必须是
*redis.TimeoutErr或等价的context.DeadlineExceeded,否则中间件(如 Gin 的 recovery)可能不识别
缓存穿透/雪崩/击穿的单元测试得靠控制 mock 返回值
这类问题本质是业务逻辑对 Redis 返回值的反应,不是 Redis 本身行为。比如缓存穿透:查不到 DB 也不写空值,导致反复打 DB。测试点不在 Redis,而在你的 service 层是否按约定处理了 redis.Nil。
关键控制点:
- 让 mock 的
Get方法返回redis.Nil(不是nil error,而是err == redis.Nil) - 检查后续是否调用了 DB 查询、是否执行了
Set(含空值)、是否加了互斥锁 - 模拟雪崩:批量让 mock 的
Get全部返回redis.Nil,观察 DB 调用量是否被熔断或降级 - 注意:不要在测试里 assert Redis 的 key 过期时间 —— 那属于集成测试范畴;单元测试只关心“你的代码是否按条件调用了
Set并传入了预期的time.Duration”
最容易被忽略的是 redis.Nil 的构造方式:它是一个预定义变量,不是 nil,也不是 errors.New("redis: nil")。mock 返回时得用 return "", redis.Nil 才能触发业务里的 errors.Is(err, redis.Nil) 判断。











