在开发应用程序时,使用 Redis 是一种常见的方式来处理和存储数据。Redis 提供了五种常用的数据类型:String、List、Set、Hash 和 Sorted Set。除了这些数据类型之外,有时我们还需要在 Redis 中执行复杂的命令序列,这时可以使用 Lua 脚本来实现。在本篇博客中,我们将探讨如何在 Go 语言中使用 go-redis/redismock 来测试 Redis 常用的五种数据类型以及执行 Lua 脚本。

1. 简介

首先,让我们了解一下 go-redis/redismock 是什么。它是一个用于 Mock Redis 数据库的库,可以模拟 Redis 客户端的行为,无需实际连接到真实的 Redis 数据库。这样,我们可以在单元测试中更快速和可控地测试与 Redis 交互的代码。

2. 安装和设置

在开始之前,请确保您已经安装了 Go 环境,并配置好了 Go 的工作空间。

首先,我们需要安装 go-redis/redismockgo-redis/redis/v8

1
2
go get github.com/go-redis/redismock/v8
go get github.com/go-redis/redis/v8

3. 示例:测试 Redis String、List、Set、Hash 和 Sorted Set

String 类型

首先,我们将测试 Redis 中的 String 类型。我们创建一个 string_ops.go 文件,并实现相应的 Go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// string_ops.go

package main

import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)

// SetString 设置 Redis 中的 String 类型数据
func SetString(client *redis.Client, key, value string) error {
return client.Set(context.Background(), key, value, 0).Err()
}

// GetString 获取 Redis 中的 String 类型数据
func GetString(client *redis.Client, key string) (string, error) {
return client.Get(context.Background(), key).Result()
}

在这个示例中,我们编写了 SetStringGetString 函数,用于设置和获取 Redis 中的 String 类型数据。

接下来,我们编写单元测试来测试这两个函数。创建一个 string_ops_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// string_ops_test.go

package main

import (
"testing"

"github.com/go-redis/redismock/v8"
)

func TestSetAndGet_String(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "mykey"
value := "myvalue"

mock.ExpectSet(key, value).SetVal("OK")
mock.ExpectGet(key).SetVal(value)

err := SetString(client, key, value)
if err != nil {
t.Errorf("SetString failed: %s", err)
}

result, err := GetString(client, key)
if err != nil {
t.Errorf("GetString failed: %s", err)
}

if result != value {
t.Errorf("unexpected value, got: %s, want: %s", result, value)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

List 类型

接下来,我们测试 Redis 中的 List 类型。我们创建一个 list_ops.go 文件,并实现相应的 Go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// list_ops.go

package main

import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)

// ListPush 在 Redis 中的 List 类型数据上进行 push 操作
func ListPush(client *redis.Client, key string, values ...string) (int64, error) {
return client.LPush(context.Background(), key, values).Result()
}

// ListPop 在 Redis 中的 List 类型数据上进行 pop 操作
func ListPop(client *redis.Client, key string) (string, error) {
return client.LPop(context.Background(), key).Result()
}

在这个示例中,我们编写了 ListPushListPop 函数,用于在 Redis 中添加和弹出 List 类型数据。

接下来,我们编写单元测试来测试这两个函数。创建一个 list_ops_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// list_ops_test.go

package main

import (
"testing"

"github.com/go-redis/redismock/v8"
)

func TestListPushAndPop(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "mylist"

// 测试 ListPush
values := []string{"item1", "item2", "item3"}
mock.ExpectLPush(key, values).SetVal(3)

// 测试 ListPop
mock.ExpectLPop(key).SetVal("item3")

count, err := ListPush(client, key, values...)
if err != nil {
t.Errorf("ListPush failed: %s", err)
}

if count != 3 {
t.Errorf("unexpected count, got: %d, want: %d", count, 3)
}

item, err := ListPop(client, key)
if err != nil {
t.Errorf("ListPop failed: %s", err)
}

if item != "item3" {
t.Errorf("unexpected item, got: %s, want: %s", item, "item3")
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

Set 类型

接下来,我们测试 Redis 中的 Set 类型。我们创建一个 set_ops.go 文件,并实现相应的 Go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// set_ops.go

package main

import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)

// SetAdd 在 Redis 中的 Set 类型数据上进行 add 操作
func SetAdd(client *redis.Client, key string, members ...string) (int64, error) {
return client.SAdd(context.Background(), key, members).Result()
}

// SetMembers 获取 Redis 中的 Set 类型数据的所有成员
func SetMembers(client *redis.Client, key string) ([]string, error) {
return client.SMembers(context.Background(), key).Result()
}

在这个示例中,我们编写了 SetAddSetMembers 函数,用于在 Redis 中添加和获取 Set 类型数据。

接下来,我们编写单元测试来测试这两个函数。创建一个 set_ops_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// set_ops_test.go

package main

import (
"reflect"
"testing"

"github.com/go-redis/redismock/v8"
)

func TestSetAddAndMembers(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "myset"

// 测试 SetAdd
members := []string{"member1", "member2", "member3"}
mock.ExpectSAdd(key, members).SetVal(3)

// 测试 SetMembers
mock.ExpectSMembers(key).SetVal([]string{"member1", "member2", "member3"})

count, err := SetAdd(client, key, members...)
if err != nil {
t.Errorf("SetAdd failed: %s", err)
}

if count != 3 {
t.Errorf("unexpected count, got: %d, want: %d", count, 3)
}

result, err := SetMembers(client, key)
if err != nil {
t.Errorf("SetMembers failed: %s", err)
}

if !reflect.DeepEqual(result, members) {
t.Errorf("unexpected members, got: %v, want: %v", result, members)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

Hash 类型

接下来,我们测试 Redis 中的 Hash 类型。我们创建一个 hash_ops.go 文件,并实现相应的 Go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// hash_ops.go

package main

import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)

// HashSet 在 Redis 中的 Hash 类型数据上进行设置操作
func HashSet(client *redis.Client, key string, fieldValues map[string]interface{}) error {
return client.HMSet(context.Background(), key, fieldValues).Err()
}

// HashGet 获取 Redis 中的 Hash 类型数据的指定字段值
func HashGet(client *redis.Client, key, field string) (string, error) {
return client.HGet(context.Background(), key, field).Result()
}

在这个示例中,我们编写了 HashSetHashGet 函数,用于在 Redis 中设置和获取 Hash 类型数据。

接下来,我们编写单元测试来测试这两个函数。创建一个 hash_ops_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// hash_ops_test.go

package main

import (
"testing"

"github.com/go-redis/redismock/v8"
)

func TestHashSetAndGet(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "myhash"

// 测试 HashSet
fieldValues := map[string]interface{}{
"field1": "value1",
"field2": "value2",
"field3": "value3",
}
mock.ExpectHMSet(key, fieldValues).SetVal("OK")

// 测试 HashGet
field := "field2"
mock.ExpectHGet(key, field).SetVal("value2")

err := HashSet(client, key, fieldValues)
if err != nil {
t.Errorf("HashSet failed: %s", err)
}

result, err := HashGet(client, key, field)
if err != nil {
t.Errorf("HashGet failed: %s", err)
}

if result != "value2" {
t.Errorf("unexpected value, got: %s, want: %s", result, "value2")
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

Sorted Set 类型

最后,我们测试 Redis 中的 Sorted Set 类型。我们创建一个 sorted_set_ops.go 文件,并实现相应的 Go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sorted_set_ops.go

package main

import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)

// SortedSetAdd 在 Redis 中的 Sorted Set 类型数据上进行 add 操作
func SortedSetAdd(client *redis.Client, key string, members ...*redis.Z) (int64, error) {
return client.ZAdd(context.Background(), key, members...).Result()
}

// SortedSetRange 获取 Redis 中的 Sorted Set 类型数据指定范围的成员
func SortedSetRange(client *redis.Client, key string, start, stop int64) ([]string, error) {
return client.ZRange(context.Background(), key, start, stop).Result()
}

在这个示例中,我们编写了 SortedSetAddSortedSetRange 函数,用于在 Redis 中添加和获取 Sorted Set 类型数据。

接下来,我们编写单元测试来测试这两个函数。创建一个 sorted_set_ops_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// sorted_set_ops_test.go

package main

import (
"reflect"
"testing"

"github.com/go-redis/redismock/v8"
)

func TestSortedSetAddAndRange(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "mysortedset"

// 测试 SortedSetAdd
members := []*redis.Z{
&redis.Z{Score: 1, Member: "member1"},
&redis.Z{Score: 2, Member: "member2"},
&redis.Z{Score: 3, Member: "member3"},
}
mock.ExpectZAdd(key, members...).SetVal(3)

// 测试 SortedSetRange
mock.ExpectZRange(key, 0, -1).SetVal([]string{"member1", "member2", "member3"})

count, err := SortedSetAdd(client, key, members...)
if err != nil {
t.Errorf("SortedSetAdd failed: %s", err)
}

if count != 3 {
t.Errorf("unexpected count, got: %d, want: %d", count, 3)
}

result, err := SortedSetRange(client, key, 0, -1)
if err != nil {
t.Errorf("SortedSetRange failed: %s", err)
}

expected := []string{"member1", "member2", "member3"}
if !reflect.DeepEqual(result, expected) {
t.Errorf("unexpected members, got: %v, want: %v", result, expected)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

4. 执行 Lua 脚本

现在,让我们测试在 Redis 中执行 Lua 脚本。我们将使用 counter.lua 脚本,并在其中实现计数器应用。

首先,创建一个名为 counter.lua 的 Lua 脚本:

1
2
3
4
5
6
7
8
9
10
-- counter.lua

local key = KEYS[1]
local increment = tonumber(ARGV[1])

if increment > 0 then
return redis.call('INCRBY', key, increment)
else
return redis.call('GET', key)
end

在该脚本中,我们接收一个键和一个增量值作为参数。如果增量值大于 0,则在 Redis 中使用 INCRBY 命令增加计数;否则,返回该键的当前值。

接下来,我们创建一个名为 counter.go 的文件,并实现相应的 Go 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// counter.go

package main

import (
"context"
"github.com/go-redis/redis/v8"
)

// Lua 脚本用于增加计数或获取当前值
const counterLuaScript = `
local key = KEYS[1]
local increment = tonumber(ARGV[1])

if increment > 0 then
return redis.call('INCRBY', key, increment)
else
return redis.call('GET', key)
end
`

// IncreaseCounter 使用 Redis EVAL 命令执行 Lua 脚本,增加计数或获取当前值
func IncreaseCounter(client *redis.Client, key string, increment int64) (int64, error) {
// 调用 Redis EVAL 命令执行 Lua 脚本
result, err := client.Eval(context.Background(), counterLuaScript, []string{key}, increment).Result()
if err != nil {
return 0, err
}

// 转换结果为整数并返回
if intValue, ok := result.(int64); ok {
return intValue, nil
}

return 0, fmt.Errorf("unexpected result type")
}

在这个示例中,我们编写了 IncreaseCounter 函数,它使用 Redis 客户端执行我们之前创建的 Lua 脚本,并返回结果。

接下来,我们编写单元测试来测试 IncreaseCounter 函数。创建一个 counter_test.go 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// counter_test.go

package main

import (
"testing"

"github.com/go-redis/redismock/v8"
)

func TestIncreaseCounter_PositiveIncrement(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "mykey"
increment := int64(5)

mock.ExpectEval(counterLuaScript, []string{key}, increment).SetVal(increment)

result, err := IncreaseCounter(client, key, increment)
if err != nil {
t.Errorf("IncreaseCounter failed: %s", err)
}

if result != increment {
t.Errorf("unexpected result, got: %d, want: %d", result, increment)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

func TestIncreaseCounter_NegativeIncrement(t *testing.T) {
client, mock := redismock.NewClientMock()

key := "mykey"
increment := int64(-5)
currentValue := int64(10)

mock.ExpectEval(counterLuaScript, []string{key}, increment).SetVal(currentValue)

result, err := IncreaseCounter(client, key, increment)
if err != nil {
t.Errorf("IncreaseCounter failed: %s", err)
}

if result != currentValue {
t.Errorf("unexpected result, got: %d, want: %d", result, currentValue)
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}

5. 结论

在本文中,我们学习了如何使用 go-redis/redismock 在 Go 语言中测试 Redis 常用的五种数据类型以及执行 Lua 脚本。我们测试了 Redis 中的 String、List、Set、Hash 和 Sorted Set 类型,并实现了一个简单的计数器应用来测试执行 Lua 脚本的功能。通过单元测试,我们可以确保这些 Redis 操作的正确性和稳定性。使用 go-redis/redismock,我们可以在不连接到真实 Redis 数据库的情况下快速和可控地测试与 Redis 交互的代码。这为我们的应用程序开发和维护带来了便利和信心。