添加单元测试

This commit is contained in:
Yun
2025-09-17 19:30:18 +08:00
parent 4a131290d6
commit 7ceb13d438
5 changed files with 513 additions and 1 deletions
+5
View File
@@ -5,9 +5,14 @@ go 1.20
require ( require (
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.11.1
) )
require ( require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+12
View File
@@ -1,5 +1,7 @@
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@@ -10,8 +12,18 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+4
View File
@@ -212,3 +212,7 @@ func (g *GlobalLock) IsClosed() bool {
defer g.closeLock.RUnlock() defer g.closeLock.RUnlock()
return g.isClosed return g.isClosed
} }
func (l *GlobalLock) GetValue() string {
return l.value
}
+473
View File
@@ -2,12 +2,16 @@ package lockx_test
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/yuninks/lockx" "github.com/yuninks/lockx"
) )
@@ -64,3 +68,472 @@ func TestLockx(t *testing.T) {
wg.Wait() wg.Wait()
} }
// MockLogger 用于测试的模拟日志器
type MockLogger struct {
mock.Mock
}
func (m *MockLogger) Errorf(ctx context.Context, format string, args ...interface{}) {
m.Called(ctx, format, args)
}
func (m *MockLogger) Infof(ctx context.Context, format string, args ...interface{}) {
m.Called(ctx, format, args)
}
// 测试工具函数
func setupTestRedis(t *testing.T) (redis.UniversalClient, func()) {
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.1" + ":" + "6379",
Password: "123456", // no password set
DB: 0, // use default DB
})
return client, func() {
client.Close()
}
}
func TestNewGlobalLock(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
t.Run("成功创建锁", func(t *testing.T) {
ctx := context.Background()
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-key")
require.NoError(t, err)
assert.NotEmpty(t, lock.GetValue())
assert.False(t, lock.IsClosed())
})
t.Run("创建锁时UUID生成失败", func(t *testing.T) {
// 这个测试需要模拟uuid.NewV7()失败,在实际中较难触发
// 通常可以跳过或者使用mock来测试
t.Skip("很难模拟UUID生成失败的情况")
})
}
func TestGlobalLock_Lock(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("成功获取锁", func(t *testing.T) {
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-key-success")
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
defer lock.Unlock()
})
t.Run("获取已存在的锁失败", func(t *testing.T) {
// 第一个客户端获取锁
lock1, err := lockx.NewGlobalLock(ctx, redisClient, "test-key-conflict")
require.NoError(t, err)
success1, err := lock1.Lock()
require.NoError(t, err)
assert.True(t, success1)
defer lock1.Unlock()
// 第二个客户端尝试获取同一个锁
lock2, err := lockx.NewGlobalLock(ctx, redisClient, "test-key-conflict")
require.NoError(t, err)
success2, err := lock2.Lock()
require.NoError(t, err)
assert.False(t, success2)
defer lock2.Unlock()
})
t.Run("Redis连接失败", func(t *testing.T) {
// 创建无效的Redis客户端
invalidClient := redis.NewClient(&redis.Options{
Addr: "invalid:6379",
})
_, err := lockx.NewGlobalLock(ctx, invalidClient, "test-key-fail")
require.NoError(t, err)
// 设置短超时
ctxShort, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
lockWithCtx, err := lockx.NewGlobalLock(ctxShort, invalidClient, "test-key-fail")
require.NoError(t, err)
success, err := lockWithCtx.Lock()
assert.Error(t, err)
assert.False(t, success)
})
}
func TestGlobalLock_Try(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("尝试获取可用锁成功", func(t *testing.T) {
_, err := lockx.NewGlobalLock(ctx, redisClient, "test-try-success")
require.NoError(t, err)
// 使用自定义选项
customLock, err := lockx.NewGlobalLock(ctx, redisClient, "test-try-custom",
lockx.WithMaxRetryTimes(2),
lockx.WithRetryInterval(50*time.Millisecond),
)
require.NoError(t, err)
success, err := customLock.Try()
require.NoError(t, err)
assert.True(t, success)
defer customLock.Unlock()
})
t.Run("尝试获取被占用的锁失败", func(t *testing.T) {
// 先获取锁
lock1, err := lockx.NewGlobalLock(ctx, redisClient, "test-try-busy")
require.NoError(t, err)
success1, err := lock1.Lock()
require.NoError(t, err)
assert.True(t, success1)
// 另一个客户端尝试获取
lock2, err := lockx.NewGlobalLock(ctx, redisClient, "test-try-busy",
lockx.WithMaxRetryTimes(2),
lockx.WithRetryInterval(10*time.Millisecond),
)
require.NoError(t, err)
success2, err := lock2.Try()
require.NoError(t, err)
assert.False(t, success2)
lock1.Unlock()
lock2.Unlock()
})
t.Run("上下文取消", func(t *testing.T) {
ctxCancel, cancel := context.WithCancel(ctx)
lock, err := lockx.NewGlobalLock(ctxCancel, redisClient, "test-try-cancel")
require.NoError(t, err)
cancel() // 立即取消
success, err := lock.Try()
assert.True(t, errors.Is(err, context.Canceled))
assert.False(t, success)
})
}
func TestGlobalLock_Unlock(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("成功释放锁", func(t *testing.T) {
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-unlock-success")
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
err = lock.Unlock()
assert.NoError(t, err)
assert.True(t, lock.IsClosed())
// 验证锁确实被释放了
val, err := redisClient.Get(ctx, "test-unlock-success").Result()
assert.Equal(t, redis.Nil, err)
assert.Empty(t, val)
})
t.Run("重复释放锁", func(t *testing.T) {
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-unlock-twice")
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
err = lock.Unlock()
assert.NoError(t, err)
// 再次释放应该不会报错
err = lock.Unlock()
assert.NoError(t, err)
})
t.Run("释放其他客户端的锁", func(t *testing.T) {
lock1, err := lockx.NewGlobalLock(ctx, redisClient, "test-unlock-other")
require.NoError(t, err)
success, err := lock1.Lock()
require.NoError(t, err)
assert.True(t, success)
// 另一个客户端尝试释放(使用不同的value)
lock2, err := lockx.NewGlobalLock(ctx, redisClient, "test-unlock-other")
require.NoError(t, err)
err = lock2.Unlock()
assert.Error(t, err)
assert.Contains(t, err.Error(), "lock was already released or owned by another client")
lock1.Unlock()
lock2.Unlock()
})
}
func TestGlobalLock_Refresh(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("自动刷新保持锁", func(t *testing.T) {
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-refresh",
lockx.WithExpiry(2*time.Second),
lockx.WithRefreshPeriod(500*time.Millisecond),
)
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
// 等待一段时间,确保刷新机制工作
time.Sleep(3 * time.Second)
// 检查锁仍然存在
val, err := redisClient.Get(ctx, "test-refresh").Result()
require.NoError(t, err)
assert.Equal(t, lock.GetValue(), val)
// 检查TTL应该还在
ttl, err := redisClient.TTL(ctx, "test-refresh").Result()
require.NoError(t, err)
assert.True(t, ttl > 0 && ttl <= 2*time.Second)
lock.Unlock()
})
t.Run("锁丢失时停止刷新", func(t *testing.T) {
mockLogger := new(MockLogger)
mockLogger.On("Errorf", mock.Anything, mock.Anything, mock.Anything).Return()
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-refresh-lost",
lockx.WithLogger(mockLogger),
lockx.WithExpiry(1*time.Second),
)
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
// 手动删除锁来模拟锁丢失
err = redisClient.Del(ctx, "test-refresh-lost").Err()
require.NoError(t, err)
// 等待刷新周期
time.Sleep(1500 * time.Millisecond)
// 验证日志被调用
mockLogger.AssertCalled(t, "Errorf", mock.Anything, mock.Anything, mock.Anything)
lock.Unlock()
})
}
func TestGlobalLock_Concurrency(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("多 goroutine 竞争锁", func(t *testing.T) {
const numGoroutines = 10
var successCount int
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
lock, err := lockx.NewGlobalLock(ctx, redisClient, "concurrent-test")
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
if success {
mu.Lock()
successCount++
mu.Unlock()
// 持有锁一段时间
time.Sleep(100 * time.Millisecond)
lock.Unlock()
}
}(i)
}
wg.Wait()
// 应该只有一个goroutine成功获取锁
assert.Equal(t, 1, successCount)
})
}
func TestGlobalLock_ContextCancellation(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
t.Run("上下文超时", func(t *testing.T) {
ctxShort, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
lock, err := lockx.NewGlobalLock(ctxShort, redisClient, "test-context-timeout")
require.NoError(t, err)
// 等待上下文超时
time.Sleep(200 * time.Millisecond)
// 尝试操作应该失败
success, err := lock.Lock()
assert.Error(t, err)
assert.False(t, success)
})
t.Run("上下文取消时自动释放锁", func(t *testing.T) {
ctxCancel, cancel := context.WithCancel(context.Background())
lock, err := lockx.NewGlobalLock(ctxCancel, redisClient, "test-context-cancel")
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
// 取消上下文
cancel()
// 等待自动清理
time.Sleep(5 * time.Second)
// 检查锁应该被释放
val, err := redisClient.Get(context.Background(), "test-context-cancel").Result()
assert.Equal(t, redis.Nil, err)
assert.Empty(t, val)
})
}
func TestGlobalLock_CustomOptions(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("自定义配置选项", func(t *testing.T) {
mockLogger := new(MockLogger)
lock, err := lockx.NewGlobalLock(ctx, redisClient, "test-custom-options",
lockx.WithLockTimeout(5*time.Second),
lockx.WithExpiry(10*time.Second),
lockx.WithRefreshPeriod(2*time.Second),
lockx.WithMaxRetryTimes(5),
lockx.WithRetryInterval(200*time.Millisecond),
lockx.WithLogger(mockLogger),
)
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
// 检查TTL是否正确设置
ttl, err := redisClient.TTL(ctx, "test-custom-options").Result()
require.NoError(t, err)
assert.True(t, ttl > 8*time.Second && ttl <= 10*time.Second)
lock.Unlock()
})
}
func TestGlobalLock_EdgeCases(t *testing.T) {
redisClient, teardown := setupTestRedis(t)
defer teardown()
ctx := context.Background()
t.Run("空键名", func(t *testing.T) {
lock, err := lockx.NewGlobalLock(ctx, redisClient, "")
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success) // Redis允许空键名
lock.Unlock()
})
t.Run("非常长的键名", func(t *testing.T) {
longKey := string(make([]byte, 1000)) // 很长的键名
lock, err := lockx.NewGlobalLock(ctx, redisClient, longKey)
require.NoError(t, err)
success, err := lock.Lock()
require.NoError(t, err)
assert.True(t, success)
lock.Unlock()
})
}
// 基准测试
func BenchmarkGlobalLock(b *testing.B) {
t := testing.T{}
redisClient, teardown := setupTestRedis(&t)
defer teardown()
ctx := context.Background()
b.Run("获取释放锁", func(b *testing.B) {
for i := 0; i < b.N; i++ {
lock, err := lockx.NewGlobalLock(ctx, redisClient, fmt.Sprintf("benchmark-%d", i))
if err != nil {
b.Fatal(err)
}
success, err := lock.Lock()
if err != nil {
b.Fatal(err)
}
if !success {
b.Fatal("Failed to acquire lock")
}
err = lock.Unlock()
if err != nil {
b.Fatal(err)
}
}
})
}
+19 -1
View File
@@ -35,7 +35,7 @@ func InitOption(opts ...Option) {
type Option func(*option) type Option func(*option)
func WithTimeout(t time.Duration) Option { func WithLockTimeout(t time.Duration) Option {
return func(o *option) { return func(o *option) {
o.lockTimeout = t o.lockTimeout = t
} }
@@ -53,6 +53,24 @@ func WithExpiry(expiry time.Duration) Option {
} }
} }
func WithRefreshPeriod(period time.Duration) Option {
return func(o *option) {
o.RefreshPeriod = period
}
}
func WithMaxRetryTimes(times int) Option {
return func(o *option) {
o.MaxRetryTimes = times
}
}
func WithRetryInterval(interval time.Duration) Option {
return func(o *option) {
o.RetryInterval = interval
}
}
type Logger interface { type Logger interface {
Errorf(ctx context.Context, format string, v ...any) Errorf(ctx context.Context, format string, v ...any)
Infof(ctx context.Context, format string, v ...any) Infof(ctx context.Context, format string, v ...any)