Repository: zeromicro/go-zero
Pull request #2793 (2023-01-14 17:24:16)

fix(mongo): modify the generated update function and add return values for update and delete functions
This PR includes two parts:

- Change the implementation of the `Update` function from calling `m.conn.ReplaceOne` to `m.conn.UpdateOne`.

- Add return values for `Update` and `Delete` functions

First of all, I have a small team that has been using go-zero for some time. You can click [here](https://github.com/xh-polaris) to view our organization.

As for the first change, I want to say that in the original case, if I want to update a document, I must first call the Find function to get it, and then call the Update function to completely update it. **This is not atomic**, which will cause conflicts in the case of concurrency.

If you change `ReplaceOne` to `UpdateOne` and add `omitempty` to all fields' bson tag in the type definition file, **you can update the desired fields as needed** during the update. I think this is a more general update method. May I ask your opinion?

As for the second change, I want to say that as a general function (that is, with this function, developers don't need to write an extra function most of the time), deleting and updating should return the number of affected documents. For example, when I delete a document according to the ID, **I need to know at least whether the document has existed before, rather than always return a success** to me.

These two problems also exist in other database models. Do you have any suggestions for improvement?

By the way, I think go-zero can try to add some adapters to the generated CURD code of the model layer to erase the operation of various databases, so that developers can simply treat the objects in the database as a golang struct when writing logic code, and achieve complete decoupling between logic and model. I see that other people have also raised this problem, and **maybe I can work for it**.

I look forward to your suggestions. I am proud to participate in open source and am willing to participate in the discussion of engineering design.

codecov-commenter: # [Codecov](https://codecov.io/gh/zeromicro/go-zero/pull/2793?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#2793](https://codecov.io/gh/zeromicro/go-zero/pull/2793?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (648e937) into [master](https://codecov.io/gh/zeromicro/go-zero/commit/02265d0bfef08452c5a7058ded6c0e12d24bc9e7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (02265d0) will **increase** coverage by `0.00%`.
> The diff coverage is `n/a`.

> :exclamation: Current head 648e937 differs from pull request most recent head 4d8bcca. Consider uploading reports for the commit 4d8bcca to get more accurate results


| [Impacted Files](https://codecov.io/gh/zeromicro/go-zero/pull/2793?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [core/stores/sqlx/sqlconn.go](https://codecov.io/gh/zeromicro/go-zero/pull/2793?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9zdG9yZXMvc3FseC9zcWxjb25uLmdv) | `56.37% <0.00%> (-1.48%)` | :arrow_down: |
| [core/stores/sqlx/tx.go](https://codecov.io/gh/zeromicro/go-zero/pull/2793?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29yZS9zdG9yZXMvc3FseC90eC5nbw==) | `23.30% <0.00%> (+3.88%)` | :arrow_up: |


Patch:
diff --git a/core/stores/redis/redis.go b/core/stores/redis/redis.go
index c821a172..f6884efe 100644
--- a/core/stores/redis/redis.go
+++ b/core/stores/redis/redis.go
@@ -1897,17 +1897,17 @@ func (s *Redis) Zadd(key string, score int64, value string) (bool, error) {
 	return s.ZaddCtx(context.Background(), key, score, value)
 }
 
-// ZaddFloat is the implementation of redis zadd command.
-func (s *Redis) ZaddFloat(key string, score float64, value string) (bool, error) {
-	return s.ZaddFloatCtx(context.Background(), key, score, value)
-}
-
 // ZaddCtx is the implementation of redis zadd command.
 func (s *Redis) ZaddCtx(ctx context.Context, key string, score int64, value string) (
 	val bool, err error) {
 	return s.ZaddFloatCtx(ctx, key, float64(score), value)
 }
 
+// ZaddFloat is the implementation of redis zadd command.
+func (s *Redis) ZaddFloat(key string, score float64, value string) (bool, error) {
+	return s.ZaddFloatCtx(context.Background(), key, score, value)
+}
+
 // ZaddFloatCtx is the implementation of redis zadd command.
 func (s *Redis) ZaddFloatCtx(ctx context.Context, key string, score float64, value string) (
 	val bool, err error) {
@@ -2278,12 +2278,25 @@ func (s *Redis) ZrangeWithScoresByFloatCtx(ctx context.Context, key string, star
 }
 
 // ZRevRangeWithScores is the implementation of redis zrevrange command with scores.
+// Deprecated: use ZrevrangeWithScores instead.
 func (s *Redis) ZRevRangeWithScores(key string, start, stop int64) ([]Pair, error) {
-	return s.ZRevRangeWithScoresCtx(context.Background(), key, start, stop)
+	return s.ZrevrangeWithScoresCtx(context.Background(), key, start, stop)
+}
+
+// ZrevrangeWithScores is the implementation of redis zrevrange command with scores.
+func (s *Redis) ZrevrangeWithScores(key string, start, stop int64) ([]Pair, error) {
+	return s.ZrevrangeWithScoresCtx(context.Background(), key, start, stop)
 }
 
 // ZRevRangeWithScoresCtx is the implementation of redis zrevrange command with scores.
+// Deprecated: use ZrevrangeWithScoresCtx instead.
 func (s *Redis) ZRevRangeWithScoresCtx(ctx context.Context, key string, start, stop int64) (
+	val []Pair, err error) {
+	return s.ZrevrangeWithScoresCtx(ctx, key, start, stop)
+}
+
+// ZrevrangeWithScoresCtx is the implementation of redis zrevrange command with scores.
+func (s *Redis) ZrevrangeWithScoresCtx(ctx context.Context, key string, start, stop int64) (
 	val []Pair, err error) {
 	err = s.brk.DoWithAcceptable(func() error {
 		conn, err := getRedis(s)
@@ -2304,12 +2317,25 @@ func (s *Redis) ZRevRangeWithScoresCtx(ctx context.Context, key string, start, s
 }
 
 // ZRevRangeWithScoresByFloat is the implementation of redis zrevrange command with scores by float.
+// Deprecated: use ZrevrangeWithScoresByFloat instead.
 func (s *Redis) ZRevRangeWithScoresByFloat(key string, start, stop int64) ([]FloatPair, error) {
-	return s.ZRevRangeWithScoresByFloatCtx(context.Background(), key, start, stop)
+	return s.ZrevrangeWithScoresByFloatCtx(context.Background(), key, start, stop)
+}
+
+// ZrevrangeWithScoresByFloat is the implementation of redis zrevrange command with scores by float.
+func (s *Redis) ZrevrangeWithScoresByFloat(key string, start, stop int64) ([]FloatPair, error) {
+	return s.ZrevrangeWithScoresByFloatCtx(context.Background(), key, start, stop)
 }
 
 // ZRevRangeWithScoresByFloatCtx is the implementation of redis zrevrange command with scores by float.
+// Deprecated: use ZrevrangeWithScoresByFloatCtx instead.
 func (s *Redis) ZRevRangeWithScoresByFloatCtx(ctx context.Context, key string, start, stop int64) (
+	val []FloatPair, err error) {
+	return s.ZrevrangeWithScoresByFloatCtx(ctx, key, start, stop)
+}
+
+// ZrevrangeWithScoresByFloatCtx is the implementation of redis zrevrange command with scores by float.
+func (s *Redis) ZrevrangeWithScoresByFloatCtx(ctx context.Context, key string, start, stop int64) (
 	val []FloatPair, err error) {
 	err = s.brk.DoWithAcceptable(func() error {
 		conn, err := getRedis(s)
diff --git a/tools/goctl/model/mongo/readme.md b/tools/goctl/model/mongo/readme.md
index 8af5e877..87969f87 100644
--- a/tools/goctl/model/mongo/readme.md
+++ b/tools/goctl/model/mongo/readme.md
@@ -67,6 +67,7 @@ import (
 	"github.com/zeromicro/go-zero/core/stores/monc"
 	"go.mongodb.org/mongo-driver/bson"
 	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.mongodb.org/mongo-driver/mongo"
 )
 
 var prefixUserCacheKey = "cache:user:"
@@ -74,8 +75,8 @@ var prefixUserCacheKey = "cache:user:"
 type userModel interface {
 	Insert(ctx context.Context, data *User) error
 	FindOne(ctx context.Context, id string) (*User, error)
-	Update(ctx context.Context, data *User) error
-	Delete(ctx context.Context, id string) error
+	Update(ctx context.Context, data *User) (*mongo.UpdateResult, error)
+	Delete(ctx context.Context, id string) (int64, error)
 }
 
 type defaultUserModel struct {
@@ -117,21 +118,21 @@ func (m *defaultUserModel) FindOne(ctx context.Context, id string) (*User, error
 	}
 }
 
-func (m *defaultUserModel) Update(ctx context.Context, data *User) error {
+func (m *defaultUserModel) Update(ctx context.Context, data *User) (*mongo.UpdateResult, error) {
 	data.UpdateAt = time.Now()
 	key := prefixUserCacheKey + data.ID.Hex()
-	_, err := m.conn.ReplaceOne(ctx, key, bson.M{"_id": data.ID}, data)
-	return err
+	res, err := m.conn.ReplaceOne(ctx, key, bson.M{"_id": data.ID}, bson.M{"$set": data})
+	return res, err
 }
 
-func (m *defaultUserModel) Delete(ctx context.Context, id string) error {
+func (m *defaultUserModel) Delete(ctx context.Context, id string) (int64, error) {
 	oid, err := primitive.ObjectIDFromHex(id)
 	if err != nil {
-		return ErrInvalidObjectId
+		return 0, ErrInvalidObjectId
 	}
 	key := prefixUserCacheKey + id
-	_, err = m.conn.DeleteOne(ctx, key, bson.M{"_id": oid})
-	return err
+	res, err := m.conn.DeleteOne(ctx, key, bson.M{"_id": oid})
+	return res, err
 }
 
 ```
diff --git a/tools/goctl/model/mongo/template/model.tpl b/tools/goctl/model/mongo/template/model.tpl
index 31fab610..287125d9 100644
--- a/tools/goctl/model/mongo/template/model.tpl
+++ b/tools/goctl/model/mongo/template/model.tpl
@@ -8,6 +8,7 @@ import (
     {{if .Cache}}"github.com/zeromicro/go-zero/core/stores/monc"{{else}}"github.com/zeromicro/go-zero/core/stores/mon"{{end}}
     "go.mongodb.org/mongo-driver/bson"
     "go.mongodb.org/mongo-driver/bson/primitive"
+    "go.mongodb.org/mongo-driver/mongo"
 )
 
 {{if .Cache}}var prefix{{.Type}}CacheKey = "cache:{{.lowerType}}:"{{end}}
@@ -15,8 +16,8 @@ import (
 type {{.lowerType}}Model interface{
     Insert(ctx context.Context,data *{{.Type}}) error
     FindOne(ctx context.Context,id string) (*{{.Type}}, error)
-    Update(ctx context.Context,data *{{.Type}}) error
-    Delete(ctx context.Context,id string) error
+    Update(ctx context.Context,data *{{.Type}}) (*mongo.UpdateResult, error)
+    Delete(ctx context.Context,id string) (int64, error)
 }
 
 type default{{.Type}}Model struct {
@@ -59,19 +60,19 @@ func (m *default{{.Type}}Model) FindOne(ctx context.Context, id string) (*{{.Typ
     }
 }
 
-func (m *default{{.Type}}Model) Update(ctx context.Context, data *{{.Type}}) error {
+func (m *default{{.Type}}Model) Update(ctx context.Context, data *{{.Type}}) (*mongo.UpdateResult, error) {
     data.UpdateAt = time.Now()
     {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex(){{end}}
-    _, err := m.conn.ReplaceOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": data.ID}, data)
-    return err
+    res, err := m.conn.UpdateOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": data.ID}, bson.M{"$set": data})
+    return res, err
 }
 
-func (m *default{{.Type}}Model) Delete(ctx context.Context, id string) error {
+func (m *default{{.Type}}Model) Delete(ctx context.Context, id string) (int64, error) {
     oid, err := primitive.ObjectIDFromHex(id)
     if err != nil {
-        return ErrInvalidObjectId
+        return 0, ErrInvalidObjectId
     }
 	{{if .Cache}}key := prefix{{.Type}}CacheKey +id{{end}}
-    _, err = m.conn.DeleteOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": oid})
-	return err
+    res, err := m.conn.DeleteOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": oid})
+	return res, err
 }


Test patch:
diff --git a/core/stores/cache/cache_test.go b/core/stores/cache/cache_test.go
index 2e12734a..9c5b843a 100644
--- a/core/stores/cache/cache_test.go
+++ b/core/stores/cache/cache_test.go
@@ -10,6 +10,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/alicebob/miniredis/v2"
 	"github.com/stretchr/testify/assert"
 	"github.com/zeromicro/go-zero/core/errorx"
 	"github.com/zeromicro/go-zero/core/hash"
@@ -109,51 +110,85 @@ func (mc *mockedNode) TakeWithExpireCtx(ctx context.Context, val interface{}, ke
 }
 
 func TestCache_SetDel(t *testing.T) {
-	const total = 1000
-	r1, clean1, err := redistest.CreateRedis()
-	assert.Nil(t, err)
-	defer clean1()
-	r2, clean2, err := redistest.CreateRedis()
-	assert.Nil(t, err)
-	defer clean2()
-	conf := ClusterConf{
-		{
-			RedisConf: redis.RedisConf{
-				Host: r1.Addr,
-				Type: redis.NodeType,
+	t.Run("test set del", func(t *testing.T) {
+		const total = 1000
+		r1, clean1, err := redistest.CreateRedis()
+		assert.Nil(t, err)
+		defer clean1()
+		r2, clean2, err := redistest.CreateRedis()
+		assert.Nil(t, err)
+		defer clean2()
+		conf := ClusterConf{
+			{
+				RedisConf: redis.RedisConf{
+					Host: r1.Addr,
+					Type: redis.NodeType,
+				},
+				Weight: 100,
 			},
-			Weight: 100,
-		},
-		{
-			RedisConf: redis.RedisConf{
-				Host: r2.Addr,
-				Type: redis.NodeType,
+			{
+				RedisConf: redis.RedisConf{
+					Host: r2.Addr,
+					Type: redis.NodeType,
+				},
+				Weight: 100,
 			},
-			Weight: 100,
-		},
-	}
-	c := New(conf, syncx.NewSingleFlight(), NewStat("mock"), errPlaceholder)
-	for i := 0; i < total; i++ {
-		if i%2 == 0 {
-			assert.Nil(t, c.Set(fmt.Sprintf("key/%d", i), i))
-		} else {
-			assert.Nil(t, c.SetWithExpire(fmt.Sprintf("key/%d", i), i, 0))
 		}
-	}
-	for i := 0; i < total; i++ {
-		var val int
-		assert.Nil(t, c.Get(fmt.Sprintf("key/%d", i), &val))
-		assert.Equal(t, i, val)
-	}
-	assert.Nil(t, c.Del())
-	for i := 0; i < total; i++ {
-		assert.Nil(t, c.Del(fmt.Sprintf("key/%d", i)))
-	}
-	for i := 0; i < total; i++ {
-		var val int
-		assert.True(t, c.IsNotFound(c.Get(fmt.Sprintf("key/%d", i), &val)))
-		assert.Equal(t, 0, val)
-	}
+		c := New(conf, syncx.NewSingleFlight(), NewStat("mock"), errPlaceholder)
+		for i := 0; i < total; i++ {
+			if i%2 == 0 {
+				assert.Nil(t, c.Set(fmt.Sprintf("key/%d", i), i))
+			} else {
+				assert.Nil(t, c.SetWithExpire(fmt.Sprintf("key/%d", i), i, 0))
+			}
+		}
+		for i := 0; i < total; i++ {
+			var val int
+			assert.Nil(t, c.Get(fmt.Sprintf("key/%d", i), &val))
+			assert.Equal(t, i, val)
+		}
+		assert.Nil(t, c.Del())
+		for i := 0; i < total; i++ {
+			assert.Nil(t, c.Del(fmt.Sprintf("key/%d", i)))
+		}
+		assert.Nil(t, c.Del("a", "b", "c"))
+		for i := 0; i < total; i++ {
+			var val int
+			assert.True(t, c.IsNotFound(c.Get(fmt.Sprintf("key/%d", i), &val)))
+			assert.Equal(t, 0, val)
+		}
+	})
+
+	t.Run("test set del error", func(t *testing.T) {
+		r1, err := miniredis.Run()
+		assert.NoError(t, err)
+		defer r1.Close()
+		r1.SetError("mock error")
+
+		r2, err := miniredis.Run()
+		assert.NoError(t, err)
+		defer r2.Close()
+		r2.SetError("mock error")
+
+		conf := ClusterConf{
+			{
+				RedisConf: redis.RedisConf{
+					Host: r1.Addr(),
+					Type: redis.NodeType,
+				},
+				Weight: 100,
+			},
+			{
+				RedisConf: redis.RedisConf{
+					Host: r2.Addr(),
+					Type: redis.NodeType,
+				},
+				Weight: 100,
+			},
+		}
+		c := New(conf, syncx.NewSingleFlight(), NewStat("mock"), errPlaceholder)
+		assert.NoError(t, c.Del("a", "b", "c"))
+	})
 }
 
 func TestCache_OneNode(t *testing.T) {
diff --git a/core/stores/redis/redis_test.go b/core/stores/redis/redis_test.go
index a3d17f23..2c7e52e2 100644
--- a/core/stores/redis/redis_test.go
+++ b/core/stores/redis/redis_test.go
@@ -213,34 +213,43 @@ func TestRedis_Hmset(t *testing.T) {
 }
 
 func TestRedis_Hscan(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		key := "hash:test"
-		fieldsAndValues := make(map[string]string)
-		for i := 0; i < 1550; i++ {
-			fieldsAndValues["filed_"+strconv.Itoa(i)] = stringx.Randn(i)
-		}
-		err := client.Hmset(key, fieldsAndValues)
-		assert.Nil(t, err)
-
-		var cursor uint64 = 0
-		sum := 0
-		for {
-			_, _, err := New(client.Addr, badType()).Hscan(key, cursor, "*", 100)
-			assert.NotNil(t, err)
-			reMap, next, err := client.Hscan(key, cursor, "*", 100)
+	t.Run("scan", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			key := "hash:test"
+			fieldsAndValues := make(map[string]string)
+			for i := 0; i < 1550; i++ {
+				fieldsAndValues["filed_"+strconv.Itoa(i)] = stringx.Randn(i)
+			}
+			err := client.Hmset(key, fieldsAndValues)
 			assert.Nil(t, err)
-			sum += len(reMap)
-			if next == 0 {
-				break
+
+			var cursor uint64 = 0
+			sum := 0
+			for {
+				_, _, err := New(client.Addr, badType()).Hscan(key, cursor, "*", 100)
+				assert.NotNil(t, err)
+				reMap, next, err := client.Hscan(key, cursor, "*", 100)
+				assert.Nil(t, err)
+				sum += len(reMap)
+				if next == 0 {
+					break
+				}
+				cursor = next
 			}
-			cursor = next
-		}
 
-		assert.Equal(t, sum, 3100)
-		_, err = New(client.Addr, badType()).Del(key)
-		assert.NotNil(t, err)
-		_, err = client.Del(key)
-		assert.Nil(t, err)
+			assert.Equal(t, sum, 3100)
+			_, err = New(client.Addr, badType()).Del(key)
+			assert.Error(t, err)
+			_, err = client.Del(key)
+			assert.NoError(t, err)
+		})
+	})
+
+	t.Run("scan with error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Del("hash:test")
+			assert.Error(t, err)
+		})
 	})
 }
 
@@ -285,136 +294,191 @@ func TestRedis_Keys(t *testing.T) {
 }
 
 func TestRedis_HyperLogLog(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		client.Ping()
-		r := New(client.Addr)
-		_, err := New(client.Addr, badType()).Pfadd("key1", "val1")
-		assert.Error(t, err)
-		ok, err := r.Pfadd("key1", "val1")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		_, err = New(client.Addr, badType()).Pfcount("key1")
-		assert.Error(t, err)
-		val, err := r.Pfcount("key1")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(1), val)
-		ok, err = r.Pfadd("key2", "val2")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		val, err = r.Pfcount("key2")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(1), val)
-		err = New(client.Addr, badType()).Pfmerge("key3", "key1", "key2")
-		assert.Error(t, err)
-		err = r.Pfmerge("key1", "key2")
-		assert.Nil(t, err)
-		val, err = r.Pfcount("key1")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(2), val)
+	t.Run("hyperloglog", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			client.Ping()
+			r := New(client.Addr)
+			_, err := New(client.Addr, badType()).Pfadd("key1", "val1")
+			assert.Error(t, err)
+			ok, err := r.Pfadd("key1", "val1")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			_, err = New(client.Addr, badType()).Pfcount("key1")
+			assert.Error(t, err)
+			val, err := r.Pfcount("key1")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(1), val)
+			ok, err = r.Pfadd("key2", "val2")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			val, err = r.Pfcount("key2")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(1), val)
+			err = New(client.Addr, badType()).Pfmerge("key3", "key1", "key2")
+			assert.Error(t, err)
+			err = r.Pfmerge("key1", "key2")
+			assert.Nil(t, err)
+			val, err = r.Pfcount("key1")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(2), val)
+		})
+	})
+
+	t.Run("hyperloglog with error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Pfadd("key1", "val1")
+			assert.Error(t, err)
+		})
 	})
 }
 
 func TestRedis_List(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		_, err := New(client.Addr, badType()).Lpush("key", "value1", "value2")
-		assert.NotNil(t, err)
-		val, err := client.Lpush("key", "value1", "value2")
-		assert.Nil(t, err)
-		assert.Equal(t, 2, val)
-		_, err = New(client.Addr, badType()).Rpush("key", "value3", "value4")
-		assert.NotNil(t, err)
-		val, err = client.Rpush("key", "value3", "value4")
-		assert.Nil(t, err)
-		assert.Equal(t, 4, val)
-		_, err = New(client.Addr, badType()).Llen("key")
-		assert.NotNil(t, err)
-		val, err = client.Llen("key")
-		assert.Nil(t, err)
-		assert.Equal(t, 4, val)
-		_, err = New(client.Addr, badType()).Lindex("key", 1)
-		assert.NotNil(t, err)
-		value, err := client.Lindex("key", 0)
-		assert.Nil(t, err)
-		assert.Equal(t, "value2", value)
-		vals, err := client.Lrange("key", 0, 10)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value2", "value1", "value3", "value4"}, vals)
-		_, err = New(client.Addr, badType()).Lpop("key")
-		assert.NotNil(t, err)
-		v, err := client.Lpop("key")
-		assert.Nil(t, err)
-		assert.Equal(t, "value2", v)
-		val, err = client.Lpush("key", "value1", "value2")
-		assert.Nil(t, err)
-		assert.Equal(t, 5, val)
-		_, err = New(client.Addr, badType()).Rpop("key")
-		assert.NotNil(t, err)
-		v, err = client.Rpop("key")
-		assert.Nil(t, err)
-		assert.Equal(t, "value4", v)
-		val, err = client.Rpush("key", "value4", "value3", "value3")
-		assert.Nil(t, err)
-		assert.Equal(t, 7, val)
-		_, err = New(client.Addr, badType()).Lrem("key", 2, "value1")
-		assert.NotNil(t, err)
-		n, err := client.Lrem("key", 2, "value1")
-		assert.Nil(t, err)
-		assert.Equal(t, 2, n)
-		_, err = New(client.Addr, badType()).Lrange("key", 0, 10)
-		assert.NotNil(t, err)
-		vals, err = client.Lrange("key", 0, 10)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value2", "value3", "value4", "value3", "value3"}, vals)
-		n, err = client.Lrem("key", -2, "value3")
-		assert.Nil(t, err)
-		assert.Equal(t, 2, n)
-		vals, err = client.Lrange("key", 0, 10)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value2", "value3", "value4"}, vals)
-		err = New(client.Addr, badType()).Ltrim("key", 0, 1)
-		assert.Error(t, err)
-		err = client.Ltrim("key", 0, 1)
-		assert.Nil(t, err)
-		vals, err = client.Lrange("key", 0, 10)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value2", "value3"}, vals)
+	t.Run("list", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			_, err := New(client.Addr, badType()).Lpush("key", "value1", "value2")
+			assert.NotNil(t, err)
+			val, err := client.Lpush("key", "value1", "value2")
+			assert.Nil(t, err)
+			assert.Equal(t, 2, val)
+			_, err = New(client.Addr, badType()).Rpush("key", "value3", "value4")
+			assert.NotNil(t, err)
+			val, err = client.Rpush("key", "value3", "value4")
+			assert.Nil(t, err)
+			assert.Equal(t, 4, val)
+			_, err = New(client.Addr, badType()).Llen("key")
+			assert.NotNil(t, err)
+			val, err = client.Llen("key")
+			assert.Nil(t, err)
+			assert.Equal(t, 4, val)
+			_, err = New(client.Addr, badType()).Lindex("key", 1)
+			assert.NotNil(t, err)
+			value, err := client.Lindex("key", 0)
+			assert.Nil(t, err)
+			assert.Equal(t, "value2", value)
+			vals, err := client.Lrange("key", 0, 10)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value2", "value1", "value3", "value4"}, vals)
+			_, err = New(client.Addr, badType()).Lpop("key")
+			assert.NotNil(t, err)
+			v, err := client.Lpop("key")
+			assert.Nil(t, err)
+			assert.Equal(t, "value2", v)
+			val, err = client.Lpush("key", "value1", "value2")
+			assert.Nil(t, err)
+			assert.Equal(t, 5, val)
+			_, err = New(client.Addr, badType()).Rpop("key")
+			assert.NotNil(t, err)
+			v, err = client.Rpop("key")
+			assert.Nil(t, err)
+			assert.Equal(t, "value4", v)
+			val, err = client.Rpush("key", "value4", "value3", "value3")
+			assert.Nil(t, err)
+			assert.Equal(t, 7, val)
+			_, err = New(client.Addr, badType()).Lrem("key", 2, "value1")
+			assert.NotNil(t, err)
+			n, err := client.Lrem("key", 2, "value1")
+			assert.Nil(t, err)
+			assert.Equal(t, 2, n)
+			_, err = New(client.Addr, badType()).Lrange("key", 0, 10)
+			assert.NotNil(t, err)
+			vals, err = client.Lrange("key", 0, 10)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value2", "value3", "value4", "value3", "value3"}, vals)
+			n, err = client.Lrem("key", -2, "value3")
+			assert.Nil(t, err)
+			assert.Equal(t, 2, n)
+			vals, err = client.Lrange("key", 0, 10)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value2", "value3", "value4"}, vals)
+			err = New(client.Addr, badType()).Ltrim("key", 0, 1)
+			assert.Error(t, err)
+			err = client.Ltrim("key", 0, 1)
+			assert.Nil(t, err)
+			vals, err = client.Lrange("key", 0, 10)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value2", "value3"}, vals)
+		})
+	})
+
+	t.Run("list error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Llen("key")
+			assert.Error(t, err)
+
+			_, err = client.Lpush("key", "value1", "value2")
+			assert.Error(t, err)
+
+			_, err = client.Lrem("key", 2, "value1")
+			assert.Error(t, err)
+
+			_, err = client.Rpush("key", "value3", "value4")
+			assert.Error(t, err)
+		})
 	})
 }
 
 func TestRedis_Mget(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		err := client.Set("key1", "value1")
-		assert.Nil(t, err)
-		err = client.Set("key2", "value2")
-		assert.Nil(t, err)
-		_, err = New(client.Addr, badType()).Mget("key1", "key0", "key2", "key3")
-		assert.NotNil(t, err)
-		vals, err := client.Mget("key1", "key0", "key2", "key3")
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value1", "", "value2", ""}, vals)
+	t.Run("mget", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			err := client.Set("key1", "value1")
+			assert.Nil(t, err)
+			err = client.Set("key2", "value2")
+			assert.Nil(t, err)
+			_, err = New(client.Addr, badType()).Mget("key1", "key0", "key2", "key3")
+			assert.NotNil(t, err)
+			vals, err := client.Mget("key1", "key0", "key2", "key3")
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value1", "", "value2", ""}, vals)
+		})
+	})
+
+	t.Run("mget error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Mget("key1", "key0")
+			assert.Error(t, err)
+		})
 	})
 }
 
 func TestRedis_SetBit(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		_, err := New(client.Addr, badType()).SetBit("key", 1, 1)
-		assert.NotNil(t, err)
-		val, err := client.SetBit("key", 1, 1)
-		assert.Nil(t, err)
-		assert.Equal(t, 0, val)
+	t.Run("setbit", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			_, err := New(client.Addr, badType()).SetBit("key", 1, 1)
+			assert.Error(t, err)
+			val, err := client.SetBit("key", 1, 1)
+			if assert.NoError(t, err) {
+				assert.Equal(t, 0, val)
+			}
+		})
+	})
+
+	t.Run("setbit error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.SetBit("key", 1, 1)
+			assert.Error(t, err)
+		})
 	})
 }
 
 func TestRedis_GetBit(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		val, err := client.SetBit("key", 2, 1)
-		assert.Nil(t, err)
-		assert.Equal(t, 0, val)
-		_, err = New(client.Addr, badType()).GetBit("key", 2)
-		assert.NotNil(t, err)
-		v, err := client.GetBit("key", 2)
-		assert.Nil(t, err)
-		assert.Equal(t, 1, v)
+	t.Run("getbit", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			val, err := client.SetBit("key", 2, 1)
+			assert.Nil(t, err)
+			assert.Equal(t, 0, val)
+			_, err = New(client.Addr, badType()).GetBit("key", 2)
+			assert.NotNil(t, err)
+			v, err := client.GetBit("key", 2)
+			assert.Nil(t, err)
+			assert.Equal(t, 1, v)
+		})
+	})
+
+	t.Run("getbit error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.GetBit("key", 2)
+			assert.Error(t, err)
+		})
 	})
 }
 
@@ -631,86 +695,107 @@ func TestRedis_Sscan(t *testing.T) {
 }
 
 func TestRedis_Set(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		_, err := New(client.Addr, badType()).Sadd("key", 1, 2, 3, 4)
-		assert.NotNil(t, err)
-		num, err := client.Sadd("key", 1, 2, 3, 4)
-		assert.Nil(t, err)
-		assert.Equal(t, 4, num)
-		_, err = New(client.Addr, badType()).Scard("key")
-		assert.NotNil(t, err)
-		val, err := client.Scard("key")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(4), val)
-		_, err = New(client.Addr, badType()).Sismember("key", 2)
-		assert.NotNil(t, err)
-		ok, err := client.Sismember("key", 2)
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		_, err = New(client.Addr, badType()).Srem("key", 3, 4)
-		assert.NotNil(t, err)
-		num, err = client.Srem("key", 3, 4)
-		assert.Nil(t, err)
-		assert.Equal(t, 2, num)
-		_, err = New(client.Addr, badType()).Smembers("key")
-		assert.NotNil(t, err)
-		vals, err := client.Smembers("key")
-		assert.Nil(t, err)
-		assert.ElementsMatch(t, []string{"1", "2"}, vals)
-		_, err = New(client.Addr, badType()).Srandmember("key", 1)
-		assert.NotNil(t, err)
-		members, err := client.Srandmember("key", 1)
-		assert.Nil(t, err)
-		assert.Len(t, members, 1)
-		assert.Contains(t, []string{"1", "2"}, members[0])
-		_, err = New(client.Addr, badType()).Spop("key")
-		assert.NotNil(t, err)
-		member, err := client.Spop("key")
-		assert.Nil(t, err)
-		assert.Contains(t, []string{"1", "2"}, member)
-		_, err = New(client.Addr, badType()).Smembers("key")
-		assert.NotNil(t, err)
-		vals, err = client.Smembers("key")
-		assert.Nil(t, err)
-		assert.NotContains(t, vals, member)
-		_, err = New(client.Addr, badType()).Sadd("key1", 1, 2, 3, 4)
-		assert.NotNil(t, err)
-		num, err = client.Sadd("key1", 1, 2, 3, 4)
-		assert.Nil(t, err)
-		assert.Equal(t, 4, num)
-		num, err = client.Sadd("key2", 2, 3, 4, 5)
-		assert.Nil(t, err)
-		assert.Equal(t, 4, num)
-		_, err = New(client.Addr, badType()).Sunion("key1", "key2")
-		assert.NotNil(t, err)
-		vals, err = client.Sunion("key1", "key2")
-		assert.Nil(t, err)
-		assert.ElementsMatch(t, []string{"1", "2", "3", "4", "5"}, vals)
-		_, err = New(client.Addr, badType()).Sunionstore("key3", "key1", "key2")
-		assert.NotNil(t, err)
-		num, err = client.Sunionstore("key3", "key1", "key2")
-		assert.Nil(t, err)
-		assert.Equal(t, 5, num)
-		_, err = New(client.Addr, badType()).Sdiff("key1", "key2")
-		assert.NotNil(t, err)
-		vals, err = client.Sdiff("key1", "key2")
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"1"}, vals)
-		_, err = New(client.Addr, badType()).Sdiffstore("key4", "key1", "key2")
-		assert.NotNil(t, err)
-		num, err = client.Sdiffstore("key4", "key1", "key2")
-		assert.Nil(t, err)
-		assert.Equal(t, 1, num)
-		_, err = New(client.Addr, badType()).Sinter("key1", "key2")
-		assert.NotNil(t, err)
-		vals, err = client.Sinter("key1", "key2")
-		assert.Nil(t, err)
-		assert.ElementsMatch(t, []string{"2", "3", "4"}, vals)
-		_, err = New(client.Addr, badType()).Sinterstore("key4", "key1", "key2")
-		assert.NotNil(t, err)
-		num, err = client.Sinterstore("key4", "key1", "key2")
-		assert.Nil(t, err)
-		assert.Equal(t, 3, num)
+	t.Run("set", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			_, err := New(client.Addr, badType()).Sadd("key", 1, 2, 3, 4)
+			assert.NotNil(t, err)
+			num, err := client.Sadd("key", 1, 2, 3, 4)
+			assert.Nil(t, err)
+			assert.Equal(t, 4, num)
+			_, err = New(client.Addr, badType()).Scard("key")
+			assert.NotNil(t, err)
+			val, err := client.Scard("key")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(4), val)
+			_, err = New(client.Addr, badType()).Sismember("key", 2)
+			assert.NotNil(t, err)
+			ok, err := client.Sismember("key", 2)
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			_, err = New(client.Addr, badType()).Srem("key", 3, 4)
+			assert.NotNil(t, err)
+			num, err = client.Srem("key", 3, 4)
+			assert.Nil(t, err)
+			assert.Equal(t, 2, num)
+			_, err = New(client.Addr, badType()).Smembers("key")
+			assert.NotNil(t, err)
+			vals, err := client.Smembers("key")
+			assert.Nil(t, err)
+			assert.ElementsMatch(t, []string{"1", "2"}, vals)
+			_, err = New(client.Addr, badType()).Srandmember("key", 1)
+			assert.NotNil(t, err)
+			members, err := client.Srandmember("key", 1)
+			assert.Nil(t, err)
+			assert.Len(t, members, 1)
+			assert.Contains(t, []string{"1", "2"}, members[0])
+			_, err = New(client.Addr, badType()).Spop("key")
+			assert.NotNil(t, err)
+			member, err := client.Spop("key")
+			assert.Nil(t, err)
+			assert.Contains(t, []string{"1", "2"}, member)
+			_, err = New(client.Addr, badType()).Smembers("key")
+			assert.NotNil(t, err)
+			vals, err = client.Smembers("key")
+			assert.Nil(t, err)
+			assert.NotContains(t, vals, member)
+			_, err = New(client.Addr, badType()).Sadd("key1", 1, 2, 3, 4)
+			assert.NotNil(t, err)
+			num, err = client.Sadd("key1", 1, 2, 3, 4)
+			assert.Nil(t, err)
+			assert.Equal(t, 4, num)
+			num, err = client.Sadd("key2", 2, 3, 4, 5)
+			assert.Nil(t, err)
+			assert.Equal(t, 4, num)
+			_, err = New(client.Addr, badType()).Sunion("key1", "key2")
+			assert.NotNil(t, err)
+			vals, err = client.Sunion("key1", "key2")
+			assert.Nil(t, err)
+			assert.ElementsMatch(t, []string{"1", "2", "3", "4", "5"}, vals)
+			_, err = New(client.Addr, badType()).Sunionstore("key3", "key1", "key2")
+			assert.NotNil(t, err)
+			num, err = client.Sunionstore("key3", "key1", "key2")
+			assert.Nil(t, err)
+			assert.Equal(t, 5, num)
+			_, err = New(client.Addr, badType()).Sdiff("key1", "key2")
+			assert.NotNil(t, err)
+			vals, err = client.Sdiff("key1", "key2")
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"1"}, vals)
+			_, err = New(client.Addr, badType()).Sdiffstore("key4", "key1", "key2")
+			assert.NotNil(t, err)
+			num, err = client.Sdiffstore("key4", "key1", "key2")
+			assert.Nil(t, err)
+			assert.Equal(t, 1, num)
+			_, err = New(client.Addr, badType()).Sinter("key1", "key2")
+			assert.NotNil(t, err)
+			vals, err = client.Sinter("key1", "key2")
+			assert.Nil(t, err)
+			assert.ElementsMatch(t, []string{"2", "3", "4"}, vals)
+			_, err = New(client.Addr, badType()).Sinterstore("key4", "key1", "key2")
+			assert.NotNil(t, err)
+			num, err = client.Sinterstore("key4", "key1", "key2")
+			assert.Nil(t, err)
+			assert.Equal(t, 3, num)
+		})
+	})
+
+	t.Run("set with error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Sadd("key", 1, 2, 3, 4)
+			assert.Error(t, err)
+
+			_, err = client.Srem("key", 3, 4)
+			assert.Error(t, err)
+
+			_, err = client.Sunionstore("key3", "key1", "key2")
+			assert.Error(t, err)
+
+			_, err = client.Sdiffstore("key4", "key1", "key2")
+			assert.Error(t, err)
+
+			_, err = client.Sinterstore("key4", "key1", "key2")
+			assert.Error(t, err)
+		})
 	})
 }
 
@@ -794,227 +879,361 @@ func TestRedis_SetExNx(t *testing.T) {
 }
 
 func TestRedis_SetGetDelHashField(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		err := client.Hset("key", "field", "value")
-		assert.Nil(t, err)
-		_, err = New(client.Addr, badType()).Hget("key", "field")
-		assert.NotNil(t, err)
-		val, err := client.Hget("key", "field")
-		assert.Nil(t, err)
-		assert.Equal(t, "value", val)
-		_, err = New(client.Addr, badType()).Hexists("key", "field")
-		assert.NotNil(t, err)
-		ok, err := client.Hexists("key", "field")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		_, err = New(client.Addr, badType()).Hdel("key", "field")
-		assert.NotNil(t, err)
-		ret, err := client.Hdel("key", "field")
-		assert.Nil(t, err)
-		assert.True(t, ret)
-		ok, err = client.Hexists("key", "field")
-		assert.Nil(t, err)
-		assert.False(t, ok)
+	t.Run("hash", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			err := client.Hset("key", "field", "value")
+			assert.Nil(t, err)
+			_, err = New(client.Addr, badType()).Hget("key", "field")
+			assert.NotNil(t, err)
+			val, err := client.Hget("key", "field")
+			assert.Nil(t, err)
+			assert.Equal(t, "value", val)
+			_, err = New(client.Addr, badType()).Hexists("key", "field")
+			assert.NotNil(t, err)
+			ok, err := client.Hexists("key", "field")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			_, err = New(client.Addr, badType()).Hdel("key", "field")
+			assert.NotNil(t, err)
+			ret, err := client.Hdel("key", "field")
+			assert.Nil(t, err)
+			assert.True(t, ret)
+			ok, err = client.Hexists("key", "field")
+			assert.Nil(t, err)
+			assert.False(t, ok)
+		})
+	})
+
+	t.Run("hash error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Hdel("key", "field")
+			assert.Error(t, err)
+
+			_, err = client.Hincrby("key", "field", 1)
+			assert.Error(t, err)
+
+			_, err = client.HincrbyFloat("key", "field", 1)
+			assert.Error(t, err)
+
+			_, err = client.Hlen("key")
+			assert.Error(t, err)
+
+			_, err = client.Hmget("key", "field")
+			assert.Error(t, err)
+		})
 	})
 }
 
 func TestRedis_SortedSet(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		ok, err := client.ZaddFloat("key", 1, "value1")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		ok, err = client.Zadd("key", 2, "value1")
-		assert.Nil(t, err)
-		assert.False(t, ok)
-		val, err := client.Zscore("key", "value1")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(2), val)
-		_, err = New(client.Addr, badType()).Zincrby("key", 3, "value1")
-		assert.NotNil(t, err)
-		val, err = client.Zincrby("key", 3, "value1")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(5), val)
-		_, err = New(client.Addr, badType()).Zscore("key", "value1")
-		assert.NotNil(t, err)
-		val, err = client.Zscore("key", "value1")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(5), val)
-		_, err = New(client.Addr, badType()).Zadds("key")
-		assert.NotNil(t, err)
-		val, err = client.Zadds("key", Pair{
-			Key:   "value2",
-			Score: 6,
-		}, Pair{
-			Key:   "value3",
-			Score: 7,
-		})
-		assert.Nil(t, err)
-		assert.Equal(t, int64(2), val)
-		_, err = New(client.Addr, badType()).ZRevRangeWithScores("key", 1, 3)
-		assert.NotNil(t, err)
-		pairs, err := client.ZRevRangeWithScores("key", 1, 3)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []Pair{
-			{
+	t.Run("sorted set", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			ok, err := client.ZaddFloat("key", 1, "value1")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			ok, err = client.Zadd("key", 2, "value1")
+			assert.Nil(t, err)
+			assert.False(t, ok)
+			val, err := client.Zscore("key", "value1")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(2), val)
+			_, err = New(client.Addr, badType()).Zincrby("key", 3, "value1")
+			assert.NotNil(t, err)
+			val, err = client.Zincrby("key", 3, "value1")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(5), val)
+			_, err = New(client.Addr, badType()).Zscore("key", "value1")
+			assert.NotNil(t, err)
+			val, err = client.Zscore("key", "value1")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(5), val)
+			_, err = New(client.Addr, badType()).Zadds("key")
+			assert.NotNil(t, err)
+			val, err = client.Zadds("key", Pair{
 				Key:   "value2",
 				Score: 6,
-			},
-			{
-				Key:   "value1",
-				Score: 5,
-			},
-		}, pairs)
-		rank, err := client.Zrank("key", "value2")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(1), rank)
-		rank, err = client.Zrevrank("key", "value1")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(2), rank)
-		_, err = New(client.Addr, badType()).Zrank("key", "value4")
-		assert.NotNil(t, err)
-		_, err = client.Zrank("key", "value4")
-		assert.Equal(t, Nil, err)
-		_, err = New(client.Addr, badType()).Zrem("key", "value2", "value3")
-		assert.NotNil(t, err)
-		num, err := client.Zrem("key", "value2", "value3")
-		assert.Nil(t, err)
-		assert.Equal(t, 2, num)
-		ok, err = client.Zadd("key", 6, "value2")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		ok, err = client.Zadd("key", 7, "value3")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		ok, err = client.Zadd("key", 8, "value4")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		_, err = New(client.Addr, badType()).Zremrangebyscore("key", 6, 7)
-		assert.NotNil(t, err)
-		num, err = client.Zremrangebyscore("key", 6, 7)
-		assert.Nil(t, err)
-		assert.Equal(t, 2, num)
-		ok, err = client.Zadd("key", 6, "value2")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		_, err = New(client.Addr, badType()).Zadd("key", 7, "value3")
-		assert.NotNil(t, err)
-		ok, err = client.Zadd("key", 7, "value3")
-		assert.Nil(t, err)
-		assert.True(t, ok)
-		_, err = New(client.Addr, badType()).Zcount("key", 6, 7)
-		assert.NotNil(t, err)
-		num, err = client.Zcount("key", 6, 7)
-		assert.Nil(t, err)
-		assert.Equal(t, 2, num)
-		_, err = New(client.Addr, badType()).Zremrangebyrank("key", 1, 2)
-		assert.NotNil(t, err)
-		num, err = client.Zremrangebyrank("key", 1, 2)
-		assert.Nil(t, err)
-		assert.Equal(t, 2, num)
-		_, err = New(client.Addr, badType()).Zcard("key")
-		assert.NotNil(t, err)
-		card, err := client.Zcard("key")
-		assert.Nil(t, err)
-		assert.Equal(t, 2, card)
-		_, err = New(client.Addr, badType()).Zrange("key", 0, -1)
-		assert.NotNil(t, err)
-		vals, err := client.Zrange("key", 0, -1)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value1", "value4"}, vals)
-		_, err = New(client.Addr, badType()).Zrevrange("key", 0, -1)
-		assert.NotNil(t, err)
-		vals, err = client.Zrevrange("key", 0, -1)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"value4", "value1"}, vals)
-		_, err = New(client.Addr, badType()).ZrangeWithScores("key", 0, -1)
-		assert.NotNil(t, err)
-		pairs, err = client.ZrangeWithScores("key", 0, -1)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []Pair{
-			{
-				Key:   "value1",
-				Score: 5,
-			},
-			{
-				Key:   "value4",
-				Score: 8,
-			},
-		}, pairs)
-		_, err = New(client.Addr, badType()).ZrangebyscoreWithScores("key", 5, 8)
-		assert.NotNil(t, err)
-		pairs, err = client.ZrangebyscoreWithScores("key", 5, 8)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []Pair{
-			{
-				Key:   "value1",
-				Score: 5,
-			},
-			{
-				Key:   "value4",
-				Score: 8,
-			},
-		}, pairs)
-		_, err = New(client.Addr, badType()).ZrangebyscoreWithScoresAndLimit(
-			"key", 5, 8, 1, 1)
-		assert.NotNil(t, err)
-		pairs, err = client.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []Pair{
-			{
-				Key:   "value4",
-				Score: 8,
-			},
-		}, pairs)
-		pairs, err = client.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
-		assert.Nil(t, err)
-		assert.Equal(t, 0, len(pairs))
-		_, err = New(client.Addr, badType()).ZrevrangebyscoreWithScores("key", 5, 8)
-		assert.NotNil(t, err)
-		pairs, err = client.ZrevrangebyscoreWithScores("key", 5, 8)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []Pair{
-			{
-				Key:   "value4",
-				Score: 8,
-			},
-			{
-				Key:   "value1",
-				Score: 5,
-			},
-		}, pairs)
-		_, err = New(client.Addr, badType()).ZrevrangebyscoreWithScoresAndLimit(
-			"key", 5, 8, 1, 1)
-		assert.NotNil(t, err)
-		pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []Pair{
-			{
-				Key:   "value1",
-				Score: 5,
-			},
-		}, pairs)
-		pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
-		assert.Nil(t, err)
-		assert.Equal(t, 0, len(pairs))
-		_, err = New(client.Addr, badType()).Zrevrank("key", "value")
-		assert.NotNil(t, err)
-		_, _ = client.Zadd("second", 2, "aa")
-		_, _ = client.Zadd("third", 3, "bbb")
-		val, err = client.Zunionstore("union", &ZStore{
-			Keys:      []string{"second", "third"},
-			Weights:   []float64{1, 2},
-			Aggregate: "SUM",
+			}, Pair{
+				Key:   "value3",
+				Score: 7,
+			})
+			assert.Nil(t, err)
+			assert.Equal(t, int64(2), val)
+			_, err = New(client.Addr, badType()).ZRevRangeWithScores("key", 1, 3)
+			assert.NotNil(t, err)
+			_, err = client.ZRevRangeWithScores("key", 1, 3)
+			assert.Nil(t, err)
+			_, err = client.ZRevRangeWithScoresCtx(context.Background(), "key", 1, 3)
+			assert.Nil(t, err)
+			pairs, err := client.ZrevrangeWithScores("key", 1, 3)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []Pair{
+				{
+					Key:   "value2",
+					Score: 6,
+				},
+				{
+					Key:   "value1",
+					Score: 5,
+				},
+			}, pairs)
+			rank, err := client.Zrank("key", "value2")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(1), rank)
+			rank, err = client.Zrevrank("key", "value1")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(2), rank)
+			_, err = New(client.Addr, badType()).Zrank("key", "value4")
+			assert.NotNil(t, err)
+			_, err = client.Zrank("key", "value4")
+			assert.Equal(t, Nil, err)
+			_, err = New(client.Addr, badType()).Zrem("key", "value2", "value3")
+			assert.NotNil(t, err)
+			num, err := client.Zrem("key", "value2", "value3")
+			assert.Nil(t, err)
+			assert.Equal(t, 2, num)
+			ok, err = client.Zadd("key", 6, "value2")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			ok, err = client.Zadd("key", 7, "value3")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			ok, err = client.Zadd("key", 8, "value4")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			_, err = New(client.Addr, badType()).Zremrangebyscore("key", 6, 7)
+			assert.NotNil(t, err)
+			num, err = client.Zremrangebyscore("key", 6, 7)
+			assert.Nil(t, err)
+			assert.Equal(t, 2, num)
+			ok, err = client.Zadd("key", 6, "value2")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			_, err = New(client.Addr, badType()).Zadd("key", 7, "value3")
+			assert.NotNil(t, err)
+			ok, err = client.Zadd("key", 7, "value3")
+			assert.Nil(t, err)
+			assert.True(t, ok)
+			_, err = New(client.Addr, badType()).Zcount("key", 6, 7)
+			assert.NotNil(t, err)
+			num, err = client.Zcount("key", 6, 7)
+			assert.Nil(t, err)
+			assert.Equal(t, 2, num)
+			_, err = New(client.Addr, badType()).Zremrangebyrank("key", 1, 2)
+			assert.NotNil(t, err)
+			num, err = client.Zremrangebyrank("key", 1, 2)
+			assert.Nil(t, err)
+			assert.Equal(t, 2, num)
+			_, err = New(client.Addr, badType()).Zcard("key")
+			assert.NotNil(t, err)
+			card, err := client.Zcard("key")
+			assert.Nil(t, err)
+			assert.Equal(t, 2, card)
+			_, err = New(client.Addr, badType()).Zrange("key", 0, -1)
+			assert.NotNil(t, err)
+			vals, err := client.Zrange("key", 0, -1)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value1", "value4"}, vals)
+			_, err = New(client.Addr, badType()).Zrevrange("key", 0, -1)
+			assert.NotNil(t, err)
+			vals, err = client.Zrevrange("key", 0, -1)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"value4", "value1"}, vals)
+			_, err = New(client.Addr, badType()).ZrangeWithScores("key", 0, -1)
+			assert.NotNil(t, err)
+			pairs, err = client.ZrangeWithScores("key", 0, -1)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []Pair{
+				{
+					Key:   "value1",
+					Score: 5,
+				},
+				{
+					Key:   "value4",
+					Score: 8,
+				},
+			}, pairs)
+			_, err = New(client.Addr, badType()).ZrangebyscoreWithScores("key", 5, 8)
+			assert.NotNil(t, err)
+			pairs, err = client.ZrangebyscoreWithScores("key", 5, 8)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []Pair{
+				{
+					Key:   "value1",
+					Score: 5,
+				},
+				{
+					Key:   "value4",
+					Score: 8,
+				},
+			}, pairs)
+			_, err = New(client.Addr, badType()).ZrangebyscoreWithScoresAndLimit(
+				"key", 5, 8, 1, 1)
+			assert.NotNil(t, err)
+			pairs, err = client.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []Pair{
+				{
+					Key:   "value4",
+					Score: 8,
+				},
+			}, pairs)
+			pairs, err = client.ZrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
+			assert.Nil(t, err)
+			assert.Equal(t, 0, len(pairs))
+			_, err = New(client.Addr, badType()).ZrevrangebyscoreWithScores("key", 5, 8)
+			assert.NotNil(t, err)
+			pairs, err = client.ZrevrangebyscoreWithScores("key", 5, 8)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []Pair{
+				{
+					Key:   "value4",
+					Score: 8,
+				},
+				{
+					Key:   "value1",
+					Score: 5,
+				},
+			}, pairs)
+			_, err = New(client.Addr, badType()).ZrevrangebyscoreWithScoresAndLimit(
+				"key", 5, 8, 1, 1)
+			assert.NotNil(t, err)
+			pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 1)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []Pair{
+				{
+					Key:   "value1",
+					Score: 5,
+				},
+			}, pairs)
+			pairs, err = client.ZrevrangebyscoreWithScoresAndLimit("key", 5, 8, 1, 0)
+			assert.Nil(t, err)
+			assert.Equal(t, 0, len(pairs))
+			_, err = New(client.Addr, badType()).Zrevrank("key", "value")
+			assert.NotNil(t, err)
+			_, _ = client.Zadd("second", 2, "aa")
+			_, _ = client.Zadd("third", 3, "bbb")
+			val, err = client.Zunionstore("union", &ZStore{
+				Keys:      []string{"second", "third"},
+				Weights:   []float64{1, 2},
+				Aggregate: "SUM",
+			})
+			assert.Nil(t, err)
+			assert.Equal(t, int64(2), val)
+			_, err = New(client.Addr, badType()).Zunionstore("union", &ZStore{})
+			assert.NotNil(t, err)
+			vals, err = client.Zrange("union", 0, 10000)
+			assert.Nil(t, err)
+			assert.EqualValues(t, []string{"aa", "bbb"}, vals)
+			ival, err := client.Zcard("union")
+			assert.Nil(t, err)
+			assert.Equal(t, 2, ival)
+		})
+	})
+
+	t.Run("sorted set error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZaddFloat("key", 1, "value")
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zadds("key")
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zcard("key")
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zcount("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zincrby("key", 1, "value")
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zscore("key", "value")
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zrem("key", "value")
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zremrangebyscore("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Zremrangebyrank("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrangeWithScores("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrangeWithScoresByFloat("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZRevRangeWithScores("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZRevRangeWithScoresByFloat("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrangebyscoreWithScores("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrangebyscoreWithScoresByFloat("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrangebyscoreWithScoresAndLimit("key", 1, 2, 1, 1)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrangebyscoreWithScoresByFloatAndLimit("key", 1, 2, 1, 1)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrevrangebyscoreWithScores("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrevrangebyscoreWithScoresByFloat("key", 1, 2)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrevrangebyscoreWithScoresAndLimit("key", 1, 2, 1, 1)
+			assert.Error(t, err)
+		})
+
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.ZrevrangebyscoreWithScoresByFloatAndLimit("key", 1, 2, 1, 1)
+			assert.Error(t, err)
 		})
-		assert.Nil(t, err)
-		assert.Equal(t, int64(2), val)
-		_, err = New(client.Addr, badType()).Zunionstore("union", &ZStore{})
-		assert.NotNil(t, err)
-		vals, err = client.Zrange("union", 0, 10000)
-		assert.Nil(t, err)
-		assert.EqualValues(t, []string{"aa", "bbb"}, vals)
-		ival, err := client.Zcard("union")
-		assert.Nil(t, err)
-		assert.Equal(t, 2, ival)
 	})
 }
 
@@ -1031,7 +1250,11 @@ func TestRedis_SortedSetByFloat64(t *testing.T) {
 		_, _ = client.ZaddFloat("key", 10.346, "value2")
 		_, err = New(client.Addr, badType()).ZRevRangeWithScoresByFloat("key", 0, -1)
 		assert.NotNil(t, err)
-		pairs, err := client.ZRevRangeWithScoresByFloat("key", 0, -1)
+		_, err = client.ZRevRangeWithScoresByFloat("key", 0, -1)
+		assert.Nil(t, err)
+		_, err = client.ZRevRangeWithScoresByFloatCtx(context.Background(), "key", 0, -1)
+		assert.Nil(t, err)
+		pairs, err := client.ZrevrangeWithScoresByFloat("key", 0, -1)
 		assert.Nil(t, err)
 		assert.EqualValues(t, []FloatPair{
 			{
@@ -1200,6 +1423,24 @@ func TestRedisEvalSha(t *testing.T) {
 	})
 }
 
+func TestRedis_Ttl(t *testing.T) {
+	t.Run("TTL", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			if assert.NoError(t, client.Set("key", "value")) {
+				_, err := client.Ttl("key")
+				assert.NoError(t, err)
+			}
+		})
+	})
+
+	t.Run("TTL error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.Ttl("key")
+			assert.Error(t, err)
+		})
+	})
+}
+
 func TestRedisToPairs(t *testing.T) {
 	pairs := toPairs([]red.Z{
 		{
@@ -1293,13 +1534,15 @@ func TestRedisBlpop(t *testing.T) {
 }
 
 func TestRedisBlpopEx(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		client.Ping()
-		var node mockedNode
-		_, _, err := client.BlpopEx(nil, "foo")
-		assert.NotNil(t, err)
-		_, _, err = client.BlpopEx(node, "foo")
-		assert.NotNil(t, err)
+	t.Run("blpopex", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			client.Ping()
+			var node mockedNode
+			_, _, err := client.BlpopEx(nil, "foo")
+			assert.Error(t, err)
+			_, _, err = client.BlpopEx(node, "foo")
+			assert.Error(t, err)
+		})
 	})
 }
 
@@ -1315,51 +1558,80 @@ func TestRedisBlpopWithTimeout(t *testing.T) {
 }
 
 func TestRedisGeo(t *testing.T) {
-	runOnRedis(t, func(client *Redis) {
-		client.Ping()
-		geoLocation := []*GeoLocation{{Longitude: 13.361389, Latitude: 38.115556, Name: "Palermo"},
-			{Longitude: 15.087269, Latitude: 37.502669, Name: "Catania"}}
-		v, err := client.GeoAdd("sicily", geoLocation...)
-		assert.Nil(t, err)
-		assert.Equal(t, int64(2), v)
-		_, err = New(client.Addr, badType()).GeoDist("sicily", "Palermo", "Catania", "m")
-		assert.Error(t, err)
-		v2, err := client.GeoDist("sicily", "Palermo", "Catania", "m")
-		assert.Nil(t, err)
-		assert.Equal(t, 166274, int(v2))
-		// GeoHash not support
-		_, err = New(client.Addr, badType()).GeoPos("sicily", "Palermo", "Catania")
-		assert.Error(t, err)
-		v3, err := client.GeoPos("sicily", "Palermo", "Catania")
-		assert.Nil(t, err)
-		assert.Equal(t, int64(v3[0].Longitude), int64(13))
-		assert.Equal(t, int64(v3[0].Latitude), int64(38))
-		assert.Equal(t, int64(v3[1].Longitude), int64(15))
-		assert.Equal(t, int64(v3[1].Latitude), int64(37))
-		_, err = New(client.Addr, badType()).GeoRadius("sicily", 15, 37,
-			&red.GeoRadiusQuery{WithDist: true, Unit: "km", Radius: 200})
-		assert.Error(t, err)
-		v4, err := client.GeoRadius("sicily", 15, 37, &red.GeoRadiusQuery{
-			WithDist: true,
-			Unit:     "km", Radius: 200,
+	t.Run("geo", func(t *testing.T) {
+		runOnRedis(t, func(client *Redis) {
+			client.Ping()
+			geoLocation := []*GeoLocation{{Longitude: 13.361389, Latitude: 38.115556, Name: "Palermo"},
+				{Longitude: 15.087269, Latitude: 37.502669, Name: "Catania"}}
+			v, err := client.GeoAdd("sicily", geoLocation...)
+			assert.Nil(t, err)
+			assert.Equal(t, int64(2), v)
+			_, err = New(client.Addr, badType()).GeoDist("sicily", "Palermo", "Catania", "m")
+			assert.Error(t, err)
+			v2, err := client.GeoDist("sicily", "Palermo", "Catania", "m")
+			assert.Nil(t, err)
+			assert.Equal(t, 166274, int(v2))
+			// GeoHash not support
+			_, err = New(client.Addr, badType()).GeoPos("sicily", "Palermo", "Catania")
+			assert.Error(t, err)
+			v3, err := client.GeoPos("sicily", "Palermo", "Catania")
+			assert.Nil(t, err)
+			assert.Equal(t, int64(v3[0].Longitude), int64(13))
+			assert.Equal(t, int64(v3[0].Latitude), int64(38))
+			assert.Equal(t, int64(v3[1].Longitude), int64(15))
+			assert.Equal(t, int64(v3[1].Latitude), int64(37))
+			_, err = New(client.Addr, badType()).GeoRadius("sicily", 15, 37,
+				&red.GeoRadiusQuery{WithDist: true, Unit: "km", Radius: 200})
+			assert.Error(t, err)
+			v4, err := client.GeoRadius("sicily", 15, 37, &red.GeoRadiusQuery{
+				WithDist: true,
+				Unit:     "km", Radius: 200,
+			})
+			assert.Nil(t, err)
+			assert.Equal(t, int64(v4[0].Dist), int64(190))
+			assert.Equal(t, int64(v4[1].Dist), int64(56))
+			geoLocation2 := []*GeoLocation{{Longitude: 13.583333, Latitude: 37.316667, Name: "Agrigento"}}
+			_, err = New(client.Addr, badType()).GeoAdd("sicily", geoLocation2...)
+			assert.Error(t, err)
+			v5, err := client.GeoAdd("sicily", geoLocation2...)
+			assert.Nil(t, err)
+			assert.Equal(t, int64(1), v5)
+			_, err = New(client.Addr, badType()).GeoRadiusByMember("sicily", "Agrigento",
+				&red.GeoRadiusQuery{Unit: "km", Radius: 100})
+			assert.Error(t, err)
+			v6, err := client.GeoRadiusByMember("sicily", "Agrigento",
+				&red.GeoRadiusQuery{Unit: "km", Radius: 100})
+			assert.Nil(t, err)
+			assert.Equal(t, v6[0].Name, "Agrigento")
+			assert.Equal(t, v6[1].Name, "Palermo")
+		})
+	})
+
+	t.Run("geo error", func(t *testing.T) {
+		runOnRedisWithError(t, func(client *Redis) {
+			_, err := client.GeoAdd("sicily", &GeoLocation{
+				Longitude: 13.3,
+				Latitude:  38.1,
+				Name:      "Palermo",
+			})
+			assert.Error(t, err)
+
+			_, err = client.GeoDist("sicily", "Palermo", "Catania", "m")
+			assert.Error(t, err)
+
+			_, err = client.GeoRadius("sicily", 15, 37, &red.GeoRadiusQuery{
+				WithDist: true,
+			})
+			assert.Error(t, err)
+
+			_, err = client.GeoRadiusByMember("sicily", "Agrigento", &red.GeoRadiusQuery{
+				Unit: "km",
+			})
+			assert.Error(t, err)
+
+			_, err = client.GeoPos("sicily", "Palermo", "Catania")
+			assert.Error(t, err)
 		})
-		assert.Nil(t, err)
-		assert.Equal(t, int64(v4[0].Dist), int64(190))
-		assert.Equal(t, int64(v4[1].Dist), int64(56))
-		geoLocation2 := []*GeoLocation{{Longitude: 13.583333, Latitude: 37.316667, Name: "Agrigento"}}
-		_, err = New(client.Addr, badType()).GeoAdd("sicily", geoLocation2...)
-		assert.Error(t, err)
-		v5, err := client.GeoAdd("sicily", geoLocation2...)
-		assert.Nil(t, err)
-		assert.Equal(t, int64(1), v5)
-		_, err = New(client.Addr, badType()).GeoRadiusByMember("sicily", "Agrigento",
-			&red.GeoRadiusQuery{Unit: "km", Radius: 100})
-		assert.Error(t, err)
-		v6, err := client.GeoRadiusByMember("sicily", "Agrigento",
-			&red.GeoRadiusQuery{Unit: "km", Radius: 100})
-		assert.Nil(t, err)
-		assert.Equal(t, v6[0].Name, "Agrigento")
-		assert.Equal(t, v6[1].Name, "Palermo")
 	})
 }
 
@@ -1393,6 +1665,29 @@ func runOnRedis(t *testing.T, fn func(client *Redis)) {
 			_ = client.Close()
 		}
 	}()
+
+	fn(New(s.Addr()))
+}
+
+func runOnRedisWithError(t *testing.T, fn func(client *Redis)) {
+	logx.Disable()
+
+	s, err := miniredis.Run()
+	assert.NoError(t, err)
+	defer func() {
+		client, err := clientManager.GetResource(s.Addr(), func() (io.Closer, error) {
+			return nil, errors.New("should already exist")
+		})
+		if err != nil {
+			t.Error(err)
+		}
+
+		if client != nil {
+			_ = client.Close()
+		}
+	}()
+
+	s.SetError("mock error")
 	fn(New(s.Addr()))
 }
 
diff --git a/core/stores/redis/redisblockingnode_test.go b/core/stores/redis/redisblockingnode_test.go
index 80ac70ce..dc0d4083 100644
--- a/core/stores/redis/redisblockingnode_test.go
+++ b/core/stores/redis/redisblockingnode_test.go
@@ -8,12 +8,39 @@ import (
 )
 
 func TestBlockingNode(t *testing.T) {
-	r, err := miniredis.Run()
-	assert.Nil(t, err)
-	node, err := CreateBlockingNode(New(r.Addr()))
-	assert.Nil(t, err)
-	node.Close()
-	node, err = CreateBlockingNode(New(r.Addr(), Cluster()))
-	assert.Nil(t, err)
-	node.Close()
+	t.Run("test blocking node", func(t *testing.T) {
+		r, err := miniredis.Run()
+		assert.NoError(t, err)
+		defer r.Close()
+
+		node, err := CreateBlockingNode(New(r.Addr()))
+		assert.NoError(t, err)
+		node.Close()
+		// close again to make sure it's safe
+		assert.NotPanics(t, func() {
+			node.Close()
+		})
+	})
+
+	t.Run("test blocking node with cluster", func(t *testing.T) {
+		r, err := miniredis.Run()
+		assert.NoError(t, err)
+		defer r.Close()
+
+		node, err := CreateBlockingNode(New(r.Addr(), Cluster(), WithTLS()))
+		assert.NoError(t, err)
+		node.Close()
+		assert.NotPanics(t, func() {
+			node.Close()
+		})
+	})
+
+	t.Run("test blocking node with bad type", func(t *testing.T) {
+		r, err := miniredis.Run()
+		assert.NoError(t, err)
+		defer r.Close()
+
+		_, err = CreateBlockingNode(New(r.Addr(), badType()))
+		assert.Error(t, err)
+	})
 }