Go 应用程序开发过程中使用 Docker 进行集成测试

实践表明,有时程序中某个模块虽然可以单独工作,但是并不能保证多个模块组装起来也可以同时工作,于是就有了集成测试。

集成测试需要解决外部依赖问题,如 MySQL、Redis、网络等依赖,解决这些外部依赖问题最佳实践则是使用 Docker,本文就来聊聊 Go 程序如何使用 Docker 来解决集成测试中外部依赖问题。

登录程序示例

在 Web 开发中,登录需求是一个较为常见的功能。所以,本文就以登录程序为例,讲解使用 Docker 启动 Redis 进行集成测试。

登录程序如下:

go 复制代码
func Login(mobile, smsCode string, rdb *redis.Client) (string, error) {
	ctx := context.Background()

	// 查找验证码
	captcha, err := GetSmsCaptchaFromRedis(ctx, rdb, mobile)
	if err != nil {
		if err == redis.Nil {
			return "", fmt.Errorf("invalid sms code or expired")
		}
		return "", err
	}

	if captcha != smsCode {
		return "", fmt.Errorf("invalid sms code")
	}

	token, _ := GenerateToken(32)
	err = SetAuthTokenToRedis(ctx, rdb, token, mobile)
	if err != nil {
		return "", err
	}

	return token, nil
}

可以通过如下方式获取 Redis 客户端对象 rdb

go 复制代码
import "github.com/redis/go-redis/v9"

func NewRedisClient() *redis.Client {
	return redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})
}

生成随机 token 的函数定义如下:

go 复制代码
var GenerateToken = func(length int) (string, error) {
	token := make([]byte, length)
	_, err := rand.Read(token)
	if err != nil {
		return "", err
	}
	return base64.URLEncoding.EncodeToString(token)[:length], nil
}

本程序提供了如下几个操作 Reids 的函数:

go 复制代码
var (
	smsCaptchaExpire    = 5 * time.Minute
	smsCaptchaKeyPrefix = "sms:captcha:%s"

	authTokenExpire    = 24 * time.Hour
	authTokenKeyPrefix = "auth:token:%s"
)

func SetSmsCaptchaToRedis(ctx context.Context, redis *redis.Client, mobile, captcha string) error {
	key := fmt.Sprintf(smsCaptchaKeyPrefix, mobile)
	return redis.Set(ctx, key, captcha, smsCaptchaExpire).Err()
}

func GetSmsCaptchaFromRedis(ctx context.Context, redis *redis.Client, mobile string) (string, error) {
	key := fmt.Sprintf(smsCaptchaKeyPrefix, mobile)
	return redis.Get(ctx, key).Result()
}

func DeleteSmsCaptchaFromRedis(ctx context.Context, redis *redis.Client, mobile string) error {
	key := fmt.Sprintf(smsCaptchaKeyPrefix, mobile)
	return redis.Del(ctx, key).Err()
}

func SetAuthTokenToRedis(ctx context.Context, redis *redis.Client, token, mobile string) error {
	key := fmt.Sprintf(authTokenKeyPrefix, token)
	return redis.Set(ctx, key, mobile, authTokenExpire).Err()
}

func GetAuthTokenFromRedis(ctx context.Context, redis *redis.Client, token string) (string, error) {
	key := fmt.Sprintf(authTokenKeyPrefix, token)
	return redis.Get(ctx, key).Result()
}

func DeleteAuthTokenFromRedis(ctx context.Context, redis *redis.Client, token string) error {
	key := fmt.Sprintf(authTokenKeyPrefix, token)
	return redis.Del(ctx, key).Err()
}

Login 函数用法如下:

go 复制代码
func main() {
	rdb := NewRedisClient()
	token, err := Login("13800001111", "123456", rdb)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(token)
}

使用 Docker 进行集成测试

要想对 Login 函数进行集成测试,就需要解决 Reids 外部依赖问题。

在 Go 程序中,我们可以使用 testcontainers-go 这个包来解决,它可以让我们很方便的在 Docker 中启动 Reids 服务。

安装 testcontainers-go

bash 复制代码
$ go get github.com/testcontainers/testcontainers-go

我们可以在测试代码开始执行之前启动 Docker 容器来运行 Redis 服务,然后执行测试代码,最后测试代码执行完成后再停止并删除 Docker 容器。

可以定义一个 setup 函数用来准备 Docker 容器:

go 复制代码
var rdbClient *redis.Client

func setup() func() {
	ctx := context.Background()
	req := testcontainers.ContainerRequest{
		Image:        "redis:6.0.20-alpine",
		ExposedPorts: []string{"6379/tcp"},
		WaitingFor:   wait.ForLog("Ready to accept connections"),
	}
	redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		panic(fmt.Sprintf("failed to start container: %s", err.Error()))
	}

	endpoint, err := redisC.Endpoint(ctx, "")
	if err != nil {
		panic(fmt.Sprintf("failed to get endpoint: %s", err.Error()))
	}

	rdbClient = redis.NewClient(&redis.Options{
		Addr: endpoint,
	})

	// 清理 Redis 容器
	return func() {
		if err := redisC.Terminate(ctx); err != nil {
			panic(fmt.Sprintf("failed to terminate container: %s", err.Error()))
		}
	}
}

可以发现使用 testcontainers-go 启动一个 Redis 容器非常简单,我们指定了 Docker 容器镜像为 redis:6.0.20-alpine,映射端口为 6379/tcp

Redis 容器启动后将实例化的 Redis 客户端保存到全局变量 rdbClient 中,方便在测试函数中使用,setup 函数最终返回一个 teardown 函数可以清理容器。

定义 TestMain 函数如下,作为测试程序的入口:

go 复制代码
func TestMain(m *testing.M) {
	teardown := setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}

为了测试 Login 函数,我们需要在 Reids 中准备一些测试数据,因为 Login 函数内部需要查询 Reids 中的验证码,所以可以定义一个 setupLogin 函数来实现:

go 复制代码
func setupLogin(tb testing.TB) func(tb testing.TB) {
	// 准备测试数据
	err := SetSmsCaptchaToRedis(context.Background(), rdbClient, "18900001111", "123456")
	assert.NoError(tb, err)

	// 清理测试数据
	return func(tb testing.TB) {
		err := DeleteSmsCaptchaFromRedis(context.Background(), rdbClient, "18900001111")
		assert.NoError(tb, err)

		err = DeleteAuthTokenFromRedis(context.Background(), rdbClient, "token")
		assert.NoError(tb, err)
	}
}

setupLogin 函数返回 teardownLogin 函数用来清理 Redis 中的测试数据,防止当有多个测试函数时互相影响。

现在可以编写 Login 函数的测试代码了:

go 复制代码
func TestLogin(t *testing.T) {
	teardownLogin := setupLogin(t)
	defer teardownLogin(t)

	// 测试登录成功情况
	token, err := Login("18900001111", "123456", rdbClient)
	assert.NoError(t, err)
	assert.Equal(t, "token", token)

	// 检查 Redis 中是否存在 token
	mobile, err := GetAuthTokenFromRedis(context.Background(), rdbClient, "token")
	assert.NoError(t, err)
	assert.Equal(t, "18900001111", mobile)
}

TestLogin 函数非常简单,这得益于前期的准备工作做的非常全面。

使用 go test 来执行测试函数:

bash 复制代码
$ go test -v                    
2023/07/26 20:48:12 github.com/testcontainers/testcontainers-go - Connected to docker: 
  Server Version: 20.10.21
  API Version: 1.41
  Operating System: Docker Desktop
  Total Memory: 7851 MB
2023/07/26 20:48:12 🐳 Creating container for image docker.io/testcontainers/ryuk:0.5.1
2023/07/26 20:48:12 ✅ Container created: a261dc723001
2023/07/26 20:48:12 🐳 Starting container: a261dc723001
2023/07/26 20:48:12 ✅ Container started: a261dc723001
2023/07/26 20:48:12 🚧 Waiting for container id a261dc723001 image: docker.io/testcontainers/ryuk:0.5.1. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms}
2023/07/26 20:48:13 🐳 Creating container for image redis:6.0.20-alpine
2023/07/26 20:48:13 ✅ Container created: 6420ead815a0
2023/07/26 20:48:13 🐳 Starting container: 6420ead815a0
2023/07/26 20:48:13 ✅ Container started: 6420ead815a0
2023/07/26 20:48:13 🚧 Waiting for container id 6420ead815a0 image: redis:6.0.20-alpine. Waiting for: &{timeout:<nil> Log:Ready to accept connections Occurrence:1 PollInterval:100ms}
=== RUN   TestLogin
--- PASS: TestLogin (0.01s)
PASS
2023/07/26 20:48:13 🐳 Terminating container: 6420ead815a0
2023/07/26 20:48:13 🚫 Container terminated: 6420ead815a0
ok      github.com/jianghushinian/test/db/redis 1.630s

测试通过。

总结

我们使用 testcontainers-go 包实现了在 Go 程序中启动一个 Docker 容器,以此解决了集成测试中依赖外部 Redis 问题。

可以发现,Docker 非常适合集成测试,使用 Dokcer 来辅助集成测试是 Go 应用程序集成测试的最佳实践。而完善的集成测试,可以确保应用程序的可靠性、可扩展性和可维护性。

相关推荐
Liquad Li几秒前
ABP vNext 标准分层解决方案项目结构完整解析
后端
半夜燃烧的香烟1 分钟前
docker 安装minio nginx,配置nginx根据文根路由minio展示图片
java·nginx·docker
布朗克16829 分钟前
39 Spring Boot Web实战
前端·spring boot·后端·实战
qiuziqiqi33 分钟前
ocker-compose.yml 和Dockerfile 区别
运维·docker·容器
西安邮电大学34 分钟前
有关数组的经典算法题
java·后端·其他·算法·面试
山东点狮信息科技有限公司34 分钟前
点狮HRM-HRM系统安全体系与数据保护方案
后端·安全·spring·spring cloud·微服务·系统安全·资产
杰克逊的日记37 分钟前
如何在不影响业务的情况下对K8S集群升级
云原生·容器·kubernetes
摇滚侠1 小时前
SpringMVC 入门到实战 SpringMVC 的执行流程 96
java·后端·spring·maven·intellij-idea
“码”力全开1 小时前
【架构深探】基于Docker与GB28181/RTSP的边缘计算AI视频管理平台:异构算力调度与源码交付实践
人工智能·docker·架构
布朗克1681 小时前
38 Spring Boot入门——自动配置、核心注解与Starter机制
java·spring boot·后端