概述

本模板提供了一个使用 redismock/v8 库进行 Redis 相关测试的标准结构,适用于需要模拟 Redis 操作的单元测试场景。

依赖

import (
	"context"
	"fmt"
	"testing"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/go-redis/redismock/v8"
	"github.com/stretchr/testify/assert"
)

模板结构

1. Mock Redis 结构体

// mockRedis 实现 Redis 接口的 mock 结构体
type mockRedis struct {
	redis.UniversalClient
}

// 如果你的业务接口中定义了额外的方法(如 Lock),也需要在这里实现
// Lock 实现 RedisLock 接口的 Lock 方法
func (m *mockRedis) Lock(ctx context.Context, key string, expires time.Duration) (customerredis.UnLocker, error) {
	return nil, nil
}

说明:如果你的业务代码直接依赖 redis.UniversalClient 接口,则无需自定义 mockRedis,可以直接使用
redismock.NewClientMock() 返回的 *redis.Client。

2. 测试函数模板

以下是一个完整的测试函数模板,包含测试用例表结构、mock 设置、测试执行和结果验证。

// TestXXX 测试描述
func TestXXX(t *testing.T) {
	t.Parallel()

	// 创建测试实例
	domain := &YourDomainImpl{}

	// 测试用例
	testCases := []struct {
		name        string
		// 测试参数
		limitRules  []YourLimitRule
		customerID  int64
		// 预期结果
		expected    YourResponse
		expectedErr bool
		// Mock 设置函数
		setupMock   func(*YourDomainImpl, redismock.ClientMock, int64, time.Time)
	}{
		{
			name: "测试场景1",
			limitRules: []YourLimitRule{
				{CycleType: 1, MaxTimes: 5}, // 按天
				{CycleType: 2, MaxTimes: 3}, // 按月
			},
			customerID: 1001,
			expected: YourResponse{
				IsRestricted: true,
				RestrictType: 2,
				CurrentCount: 4,
				MaxTimes:     3,
			},
			expectedErr: false,
			setupMock: func(domain *YourDomainImpl, mock redismock.ClientMock, customerID int64, now time.Time) {
				// 设置 Redis Get 操作的 mock
				cycleInfo := domain.calculateCycle(2, now) // 按月
				cacheKey := domain.buildCacheKey(customerID, 2, cycleInfo.Start)
				mock.ExpectGet(cacheKey).SetVal("4") // 返回达到限制的值
			},
		},
		{
			name: "测试场景2 - Redis返回nil",
			limitRules: []YourLimitRule{
				{CycleType: 2, MaxTimes: 5}, // 按月
				{CycleType: 1, MaxTimes: 3}, // 按天
			},
			customerID: 1004,
			expected: YourResponse{
				IsRestricted: false,
				RestrictType: 0,
				CurrentCount: 0,
				MaxTimes:     0,
			},
			expectedErr: false,
			setupMock: func(domain *YourDomainImpl, mock redismock.ClientMock, customerID int64, now time.Time) {
				// 为所有规则设置mock,都返回redis.Nil
				limitRules := []YourLimitRule{
					{CycleType: 2, MaxTimes: 5}, // 按月
					{CycleType: 1, MaxTimes: 3}, // 按天
				}
				for _, rule := range limitRules {
					cycleInfo := domain.calculateCycle(rule.CycleType, now)
					cacheKey := domain.buildCacheKey(customerID, rule.CycleType, cycleInfo.Start)
					mock.ExpectGet(cacheKey).SetErr(redis.Nil)
				}
			},
		},
		{
			name: "测试场景3 - Redis查询失败",
			limitRules: []YourLimitRule{
				{CycleType: 2, MaxTimes: 5}, // 按月
			},
			customerID:  1005,
			expected:    YourResponse{},
			expectedErr: true,
			setupMock: func(domain *YourDomainImpl, mock redismock.ClientMock, customerID int64, now time.Time) {
				// 模拟Redis Get操作失败
				cycleInfo := domain.calculateCycle(2, now)
				cacheKey := domain.buildCacheKey(customerID, 2, cycleInfo.Start)
				mock.ExpectGet(cacheKey).SetErr(fmt.Errorf("Redis连接失败"))
			},
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			// 创建Redis mock
			client, mock := redismock.NewClientMock()
			mockRedis := &mockRedis{UniversalClient: client}

			// 模拟Redis Get操作
			ctx := context.Background()
			now := time.Now()

			// 设置mock预期
			tc.setupMock(domain, mock, tc.customerID, now)

			// 准备请求参数
			req := &YourRequest{
				CustomerID: tc.customerID,
				ShopID:     1,
				CardType:   1,
			}

			// 执行测试
			result, err := domain.yourMethod(ctx, mockRedis, tc.limitRules, req)

			// 验证结果
			if tc.expectedErr {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), "错误信息")
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tc.expected.IsRestricted, result.IsRestricted)
				assert.Equal(t, tc.expected.RestrictType, result.RestrictType)
				if tc.expected.IsRestricted {
					assert.Equal(t, tc.expected.CurrentCount, result.CurrentCount)
					assert.Equal(t, tc.expected.MaxTimes, result.MaxTimes)
				}
			}

			// 验证所有的mock预期都被满足
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Errorf("未满足的mock预期: %v", err)
			}
		})
	}
}

使用说明

  • 替换占位符
    • YourDomainImpl - 替换为你的领域、服务等实现结构体
    • yourMethod - 替换为你要测试的方法名
    • TestXXX - 替换为你的测试函数名
  • 自定义测试用例
    • 根据具体业务场景添加或修改测试用例
    • 为每个测试用例设置适当的 setupMock 函数来模拟 Redis 操作
  • 扩展模拟操作
    除了 ExpectGet,还可以使用其他 Redis 操作的模拟,如:
    • mock.ExpectSet(key, value, expiration) - 模拟 SET 操作
    • mock.ExpectIncr(key) - 模拟 INCR 操作
    • mock.ExpectExpire(key, expiration) - 模拟 EXPIRE 操作
    • 支持管道(pipelining)和事务(Tx)的模拟。

最佳实践

  • 并行测试:使用 t.Parallel() 提高测试执行效率
  • 完整的错误处理:测试 Redis 操作失败的场景
  • 边界情况:测试 Redis 返回 nil 的情况
  • 验证 mock 预期:使用 mock.ExpectationsWereMet() 确保所有 mock 操作都被调用
  • 清晰的测试用例命名:使用描述性的测试用例名称,便于理解测试场景

示例场景

1. 测试限流场景

// 测试用例:按月规则超过限制
{
	name: "按月规则超过限制",
	limitRules: []YourLimitRule{
		{CycleType: 1, MaxTimes: 5}, // 按天
		{CycleType: 2, MaxTimes: 3}, // 按月
	},
	customerID: 1001,
	expected: YourResponse{
		IsRestricted: true,
		RestrictType: 2,
		CurrentCount: 4,
		MaxTimes:     3,
	},
	expectedErr: false,
	setupMock: func(domain *YourDomainImpl, mock redismock.ClientMock, customerID int64, now time.Time) {
		// 只为按月规则设置mock,返回达到限制的值
		cycleInfo := domain.calculateCycle(2, now) // 按月
		cacheKey := domain.buildCacheKey(customerID, 2, cycleInfo.Start)
		mock.ExpectGet(cacheKey).SetVal("4") // 达到限制
	},
}

2. 测试缓存不存在场景

// 测试用例:Redis返回nil(缓存中没有数据)
{
	name: "Redis返回nil(缓存中没有数据)",
	limitRules: []YourLimitRule{
		{CycleType: 2, MaxTimes: 5}, // 按月
		{CycleType: 1, MaxTimes: 3}, // 按天
	},
	customerID: 1004,
	expected: YourResponse{
		IsRestricted: false,
		RestrictType: 0,
		CurrentCount: 0,
		MaxTimes:     0,
	},
	expectedErr: false,
	setupMock: func(domain *YourDomainImpl, mock redismock.ClientMock, customerID int64, now time.Time) {
		// 为所有规则设置mock,都返回redis.Nil
		limitRules := []YourLimitRule{
			{CycleType: 2, MaxTimes: 5}, // 按月
			{CycleType: 1, MaxTimes: 3}, // 按天
		}
		for _, rule := range limitRules {
			cycleInfo := domain.calculateCycle(rule.CycleType, now)
			cacheKey := domain.buildCacheKey(customerID, rule.CycleType, cycleInfo.Start)
			mock.ExpectGet(cacheKey).SetErr(redis.Nil)
		}
	},
}

3. 测试Redis错误场景

// 测试用例:Redis查询失败
{
	name: "Redis查询失败",
	limitRules: []YourLimitRule{
		{CycleType: 2, MaxTimes: 5}, // 按月
	},
	customerID:  1005,
	expected:    YourResponse{},
	expectedErr: true,
	setupMock: func(domain *YourDomainImpl, mock redismock.ClientMock, customerID int64, now time.Time) {
		// 模拟Redis Get操作失败
		cycleInfo := domain.calculateCycle(2, now)
		cacheKey := domain.buildCacheKey(customerID, 2, cycleInfo.Start)
		mock.ExpectGet(cacheKey).SetErr(fmt.Errorf("Redis连接失败"))
	},
}

总结

本模板提供了一个完整的 Redis Mock 测试框架,可以帮助开发者快速编写针对 Redis 操作的单元测试。通过使用 redismock/v8 库,我们可以方便地模拟各种 Redis 操作场景,包括正常返回、返回 nil 以及操作失败等情况,从而确保代码在各种 Redis 状态下都能正确处理。

使用此模板时,只需根据具体业务场景替换相应的占位符,并添加适当的测试用例即可。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐