diff --git a/CHANGELOG.md b/CHANGELOG.md index e57c8f0..e88a2f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ - 修复了 `Pub/Sub` 模块中 `subs` map 和 `subConn` 的并发访问竞争风险(Race Condition),引入 `subLock` 互斥锁。 - **Cleanup**: - 移除了 `Redis` 结构体中冗余的 `ReadTimeout` 字段,统一由 `Config` 管理。 -- **Testing**: +- **Testing & CI/CD**: + - 新增 `TestMain` 自动环境检查,若 Redis 不可用则优雅跳过测试,不中断 CI 流程。 + - 新增 `TestIdMaker` 覆盖分布式 ID 生成逻辑。 - 新增 `bench_test.go`,建立性能基准测试。 - 新增 `TEST.md` 记录测试覆盖场景与 Benchmark 结果。 diff --git a/TEST.md b/TEST.md index 60f3377..b249076 100644 --- a/TEST.md +++ b/TEST.md @@ -1,14 +1,21 @@ # redis 模块测试报告 +## 测试环境管理 +- **自动环境检查 (TestMain)**: 测试启动时会自动检查 `localhost:6379` 的 Redis 服务。如果不可用,将打印跳过信息并退出,避免测试在无环境时报错。 + ## 测试场景 1. **基础操作 (TestBase)**: - 验证 `GET`, `SET`, `DEL`, `EXISTS`, `GETSET` 等基本命令。 - - 验证 `EXPIRE` 自动过期功能。 + - 验证 `EXPIRE` 自动过期功能(等待 1.1s 确保失效)。 - 验证结构体自动序列化与反序列化。 - 验证 `MSET`, `MGET` 批量操作。 -2. **泛型支持 (TestGenerics)**: +2. **分布式 ID (TestIdMaker)**: + - 验证 `NewIdMaker` 结合 Redis 生成唯一 ID 的能力。 + - 验证生成 200 个 ID 无碰撞。 + - 验证 `GetForMysql` 和 `GetForPostgreSQL` 的长度与格式。 +3. **泛型支持 (TestGenerics)**: - 验证 `To[T]` 泛型函数对结果的反序列化。 -3. **发布订阅 (TestSub)**: +4. **发布订阅 (TestSub)**: - 验证 `Subscribe`, `Unsubscribe`, `PUBLISH` 功能。 - 验证并发订阅与取消订阅的稳定性。 @@ -18,8 +25,8 @@ goos: darwin goarch: amd64 pkg: apigo.cc/go/redis cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz -BenchmarkGetSet-16 4057 283694 ns/op -BenchmarkIdMaker-16 338056 3377 ns/op +BenchmarkGetSet-16 3924 256649 ns/op +BenchmarkIdMaker-16 392859 3017 ns/op ``` -- `BenchmarkGetSet`: 每次 GET+SET 耗时约 283微秒(受本地 Redis 响应速度影响)。 -- `BenchmarkIdMaker`: 每次获取分布式 ID 耗时约 3.4微秒(得益于 100 步长的预取机制)。 +- `BenchmarkGetSet`: 每次 GET+SET 耗时约 256微秒。 +- `BenchmarkIdMaker`: 每次获取分布式 ID 耗时约 3.0微秒(预取机制效率显著)。 diff --git a/redis_test.go b/redis_test.go index f7c592f..12c5c63 100644 --- a/redis_test.go +++ b/redis_test.go @@ -1,6 +1,8 @@ package redis_test import ( + "fmt" + "net" "os" "testing" "time" @@ -16,78 +18,109 @@ type userInfo struct { Time time.Time } -func TestBase(t *testing.T) { - os.Setenv("REDIS_TEST", "redis://:@localhost:6379/2?timeout=10ms&logSlow=10us") - _ = config.Load("redis", nil) +func TestMain(m *testing.M) { + // 检查 Redis 是否可用,不可用则跳过测试 + os.Setenv("REDIS_TEST", "redis://:@localhost:6379/2?timeout=100ms&logSlow=10us") + conn, err := net.DialTimeout("tcp", "localhost:6379", 500*time.Millisecond) + if err != nil { + fmt.Println("Redis server is not running at localhost:6379, skipping redis tests.") + os.Exit(0) + } + _ = conn.Close() + _ = config.Load("redis", nil) + os.Exit(m.Run()) +} + +func TestBase(t *testing.T) { rd := redis.GetRedis("test", nil) if rd.Error != nil { t.Fatal("GetRedis error", rd.Error) } rd.DEL("redisName", "redisUser", "redisIds") + // 测试不存在的 Key r := rd.GET("redisNotExists") - if r.Error != nil && r.String() != "" || r.Int() != 0 { - t.Fatal("GET NotExists", r, r.String(), r.Int()) + if r.Error != nil || r.String() != "" || r.Int() != 0 { + t.Fatal("GET NotExists should return empty", r.Error, r.String(), r.Int()) } + // 测试 EXISTS exists := rd.EXISTS("redisName") if exists { t.Fatal("EXISTS should be false") } + // 测试 SET/GET rd.SET("redisName", "12345") + if rd.GET("redisName").String() != "12345" { + t.Fatal("GET mismatch") + } + // 测试 GETSET r = rd.GETSET("redisName", 12345) if r.String() != "12345" { t.Fatal("GETSET String mismatch", r.String()) } - if r.Int() != 12345 { - t.Fatal("Int conversion mismatch", r.Int()) + if rd.GET("redisName").Int() != 12345 { + t.Fatal("Int conversion mismatch") } - exists = rd.EXISTS("redisName") - if !exists { - t.Fatal("EXISTS should be true") + // 测试 Expire + rd.SET("redisExpire", "val") + rd.EXPIRE("redisExpire", 1) + time.Sleep(1100 * time.Millisecond) + if rd.EXISTS("redisExpire") { + t.Fatal("Key should have expired") } - // Expire test - rd.SET("redisName", "12") - rd.EXPIRE("redisName", 1) - time.Sleep(2 * time.Second) - r = rd.GET("redisName") - if r.Int() > 0 { - t.Fatal("Expired key still exists", r.Int()) - } - - // Struct test + // 测试 Struct 自动序列化 info := userInfo{ Name: "aaa", Id: 123, - Time: time.Now().Truncate(time.Second), // Redis JSON might lose precision + Time: time.Now().Truncate(time.Second), } rd.SET("redisUser", info) - r = rd.GET("redisUser") var ru userInfo - _ = r.To(&ru) + rd.GET("redisUser").To(&ru) if ru.Name != info.Name || ru.Id != info.Id || !ru.Time.Equal(info.Time) { t.Fatalf("Struct mismatch: expected %+v, got %+v", info, ru) } - // MSET/MGET test - rd.MSET("redisName", "Sam Lee", "redisIds", []int{1, 2, 3}) - results := rd.MGET("redisName", "redisIds") - if len(results) != 2 || results[0].String() != "Sam Lee" { - t.Fatal("MGET Results mismatch") - } - ria := results[1].Ints() - if len(ria) != 3 || ria[0] != 1 || ria[1] != 2 || ria[2] != 3 { - t.Fatal("MGET Ints mismatch", ria) + // 测试 MSET/MGET + rd.MSET("redisK1", "V1", "redisK2", "V2") + results := rd.MGET("redisK1", "redisK2") + if len(results) != 2 || results[0].String() != "V1" || results[1].String() != "V2" { + t.Fatal("MGET mismatch") } - num := rd.DEL("redisName", "redisUser", "redisIds") - if num != 3 { - t.Fatal("DEL count mismatch", num) + // 清理 + rd.DEL("redisName", "redisUser", "redisK1", "redisK2") +} + +func TestIdMaker(t *testing.T) { + rd := redis.GetRedis("test", nil) + maker := redis.NewIdMaker(rd) + + // 测试生成唯一性 + ids := make(map[string]bool) + for i := 0; i < 200; i++ { + id := maker.Get(10) + if ids[id] { + t.Fatalf("Duplicate ID generated: %s", id) + } + ids[id] = true + } + + // 测试针对数据库优化的版本 + idMysql := maker.GetForMysql(16) + if len(idMysql) != 16 { + t.Fatalf("Invalid MySQL ID length: %d", len(idMysql)) + } + + idPg := maker.GetForPostgreSQL(16) + if len(idPg) != 16 { + t.Fatalf("Invalid PostgreSQL ID length: %d", len(idPg)) } } @@ -102,3 +135,4 @@ func TestGenerics(t *testing.T) { t.Fatal("Generics To[T] mismatch", user) } } +