实践表明,有时程序中某个模块虽然可以单独工作,但是并不能保证多个模块组装起来也可以同时工作,于是就有了集成测试。
集成测试需要解决外部依赖问题,如 MySQL、Redis、网络等依赖,解决这些外部依赖问题最佳实践则是使用 Docker,本文就来聊聊 Go 程序如何使用 Docker 来解决集成测试中外部依赖问题。
登录程序示例
在 Web 开发中,登录需求是一个较为常见的功能。所以,本文就以登录程序为例,讲解使用 Docker 启动 Redis 进行集成测试。
登录程序如下:
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
:
import "github.com/redis/go-redis/v9"
func NewRedisClient() *redis.Client {
return redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
}
生成随机 token
的函数定义如下:
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 的函数:
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
函数用法如下:
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
:
$ go get github.com/testcontainers/testcontainers-go
我们可以在测试代码开始执行之前启动 Docker 容器来运行 Redis 服务,然后执行测试代码,最后测试代码执行完成后再停止并删除 Docker 容器。
可以定义一个 setup
函数用来准备 Docker 容器:
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
函数如下,作为测试程序的入口:
func TestMain(m *testing.M) {
teardown := setup()
code := m.Run()
teardown()
os.Exit(code)
}
为了测试 Login
函数,我们需要在 Reids 中准备一些测试数据,因为 Login
函数内部需要查询 Reids 中的验证码,所以可以定义一个 setupLogin
函数来实现:
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 中的测试数据,防止当有多个测试函数时互相影响。
为了便于测试,可以将 GenerateToken
函数返回值固定下来:
func init() {
GenerateToken = func(length int) (string, error) {
return "token", nil
}
}
现在可以编写 Login
函数的测试代码了:
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
来执行测试函数:
$ 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: 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: 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 应用程序集成测试的最佳实践。而完善的集成测试,可以确保应用程序的可靠性、可扩展性和可维护性。