在开发应用程序时,使用 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

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 代码:

// 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 文件:

// 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 代码:

// 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 文件:

// 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 代码:

// 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 文件:

// 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 代码:

// 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 文件:

// 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 代码:

// 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 文件:

// 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 脚本:

-- 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 代码:

// 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 文件:

// 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 交互的代码。这为我们的应用程序开发和维护带来了便利和信心。