请点击这里阅读原文 点击这里阅读原文请访问 packagemain.tech
集成测试是什么?集成测试主要是为了确保不同的软件组件、子系统或应用程序在组合后能够很好地配合得当。
这在测试金字塔中是一个非常重要的步骤,可以帮助识别组件组合时可能出现的问题,比如兼容性问题、数据一致性问题和通信方面的问题。
在本文中,我们将集成测试定义为我们的后端应用程序与外部系统(如数据库和缓存)之间的通信测试。
集成测试的几种运行方法此图仅展示了三种类型的测试,但实际上还有很多其他的测试类型,例如组件测试、系统测试、负载测试等等。
单元测试运行起来非常简单(你只需像平时运行代码那样运行测试),而集成测试通常需要搭建一些测试环境。在我工作过的几家公司里,我见到过解决集成测试环境问题的方法。
选项 1. 使用一次性数据库和其他依赖关系,在集成测试开始前必须准备好这些依赖关系,并在测试完成后进行销毁。根据你的应用程序的复杂程度,这个选项可能需要较大的工作量,因为你必须确保基础设施的正常运行,并且数据需预先配置为特定的所需状态。
选项 2. 利用现有的共享数据库及其他依赖项。可以为集成测试创建独立环境,或直接使用现有环境(如预发布环境)。不过这种方法存在不少缺点,因此我并不建议采用。由于这是个共享环境,多个测试可并行运行并同时修改数据,因此你可能会遇到数据状态不一致的情况。
选项3. 使用所需服务的内存版本或嵌入式版本进行集成测试。虽然这是一个不错的方法,但并非所有的依赖项都有内存版本,即使有,这些实现可能也不具备你的生产数据库的功能。
选项 4. 使用Testcontainers在测试代码内部启动和管理测试所需的依赖项。这确保了测试运行之间的完全隔离、可重复性和更好的CI体验。接下来我们将深入探讨这个话题。
我们的测试服务:非常简单的短链接服务为了演示我们准备的测试,我们编写了一个非常简单的用 Go 语言编写的 URL 缩短器 API,它使用 MongoDB 作为数据存储,并利用 Redis 作为读取穿透缓存。我们将测试该 API 的两个端点,这两个端点是:
- /create?url= 为给定的 URL 生成哈希并将其存入数据库。
- /get?key= 通过给定的键返回原始 URL。
我们不会过于深入探讨端点的细节,你可以在该GitHub仓库中找到完整的代码如下。不过,我们来看看我们是如何定义“server”结构体的:
类型 Server struct {
DB DB
Cache Cache
}
func NewServer(db DB, cache Cache) (*Server, error) {
if err := db.Init(); err != nil {
return nil, err
}
if err := cache.Init(); err != nil {
return nil, err
}
返回 &Server{DB: db, Cache: cache}, nil
}
NewServer 函数允许我们初始化一个服务器,该服务器具有数据库和缓存实例,这些实例实现了 DB 和 Cache 接口。
type DB interface { // 数据库接口
Init() error // 初始化
StoreURL(url string, key string) error // 存储URL
GetURL(key string) (string, error) // 获取URL
}
type Cache interface { // 缓存接口
Init() error // 初始化
Set(key string, val string) error // 设置
Get(key string) (string, bool) // 获取
}
带有模拟依赖的单元测试
因为我们把所有的依赖都定义为接口,我们可以轻松地为它们生成模拟,并在我们的单元测试中使用它们。
mockery --all --with-expecter
go test -v ./... (这将运行所有测试用例并详细输出)
借助单元测试,我们可以很好地覆盖我们应用程序的低级组件,如接口、哈希键的处理逻辑等。因此,我们只需要模拟数据库和缓存的功能调用。
_unittest.go
func TestServerWithMocks(t *testing.T) {
mockDB := mocks.NewDB(t)
mockCache := mocks.NewCache(t)
mockDB.EXPECT().Init().Return(nil)
mockDB.EXPECT().StoreURL(mock.Anything, mock.Anything).Return(nil)
mockDB.EXPECT().GetURL(mock.Anything).Return("url", nil)
mockCache.EXPECT().Init().Return(nil)
mockCache.EXPECT().Get(mock.Anything).Return("url", true)
mockCache.EXPECT().Set(mock.Anything, mock.Anything).Return(nil)
s, err := NewServer(mockDB, mockCache)
assert.NoError(t, err)
srv := httptest.NewServer(s)
defer srv.Close()
// 实际测试在此执行,详情请参阅仓库中的代码
testServer(srv, t)
}
mocks.NewDB(t) 和 mocks.NewCache(t) 是由 mockery 自动生成的,我们使用 EXPECT() 来模拟这些函数的行为。需要注意的是,我们创建了一个单独的函数 testServer(srv, t),稍后在其他测试中也会用到这个函数,但会传入不同的服务器结构体。
正如你可能已经知道的那样,这些单元测试并没有检查我们的应用与数据库/缓存之间的通信状况,所以我们可能会轻易地忽略一些非常关键的 bug。为了对我们的应用更加有信心,我们应该同时编写集成测试和单元测试,以确保我们的应用功能全面且正常运行。
带有真实依赖的集成测试
如上所述的选项1和选项2,我们可以提前准备好依赖项,并针对这些实例进行测试。一种选择是使用包含MongoDB和Redis的Docker Compose配置,在测试开始前启动,测试结束后关闭。种子数据可以是此配置的一部分,或者单独准备。
一个文件名 compose.yaml
服务:
mongodb:
镜像: mongodb/mongodb-community-server:7.0-ubi8
重启: 总是
端口:
- "27017:27017"
redis:
镜像: redis:7.4-alpine
重启: 总是
端口:
- "6379:6379"
文件名 '_realdepstest.go'
// 构建指令:使用真实依赖项
// +build realdeps
package main
// 此函数用于测试带有真实依赖项的服务器
func TestServerWithRealDependencies(t *testing.T) {
// 设置环境变量以指向本地的 MongoDB 和 Redis 实例
os.Setenv("MONGO_URI", "mongodb://localhost:27017")
os.Setenv("REDIS_URI", "redis://localhost:6379")
// 创建一个新的服务器实例,使用 MongoDB 和 Redis 依赖项
s, err := NewServer(&MongoDB{}, &Redis{})
assert.NoError(t, err)
// 启动一个新的 HTTP 服务器并将其存储在 srv 中
srv := httptest.NewServer(s)
// 延迟关闭服务器,确保测试结束后服务器关闭
defer srv.Close()
// 测试服务器功能
testServer(srv, t)
}
现在的测试不使用模拟,而是直接连接到已准备好的数据库和缓存。提示:我们添加了一个“realdeps”构建标签,所以需要明确指定这个标签来运行这些测试。
启动服务:
docker-compose up -d
运行测试:
go test -tags=realdeps -v ./...
停止服务:
docker-compose down
使用 Testcontainers 进行集成测试(Integration Tests)
不过,使用Docker Compose来创建可靠的服务依赖关系需要对Docker内部工作原理有深入理解,以及如何在容器中最佳地运行特定技术。比如,创建动态集成测试环境时,可能会遇到端口冲突,以及容器未能完全启动和可用的问题。
现在我们可以使用Testcontainers来做同样的事情,这意味着我们能够更好地控制这些临时依赖,并确保每个测试运行时它们都是隔离的。只要容器运行时兼容Docker API,你几乎可以在Testcontainers中运行任何东西。
integration_test.go
//go:build integration
// +build integration
package main
import (
"context"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go/modules/mongodb"
"github.com/testcontainers/testcontainers-go/modules/redis"
)
// 测试带有测试容器的服务器的功能
func TestServerWithTestcontainers(t *testing.T) {
// 初始化上下文
ctx := context.Background()
// 启动mongodb容器
mongodbContainer, err := mongodb.Run(ctx, "docker.io/mongodb/mongodb-community-server:7.0-ubi8")
assert.NoError(t, err)
// 关闭mongodb容器
defer mongodbContainer.Terminate(ctx)
// 启动redis容器
redisContainer, err := redis.Run(ctx, "docker.io/redis:7.4-alpine")
assert.NoError(t, err)
// 关闭redis容器
defer redisContainer.Terminate(ctx)
// 获取mongodb端点
mongodbEndpoint, _ := mongodbContainer.Endpoint(ctx, "")
// 获取redis端点
redisEndpoint, _ := redisContainer.Endpoint(ctx, "")
// 设置mongodb的环境变量
os.Setenv("MONGO_URI", "mongodb://"+mongodbEndpoint)
// 设置redis的环境变量
os.Setenv("REDIS_URI", "redis://"+redisEndpoint)
// 创建一个新的服务器实例
s, err := NewServer(&MongoDB{}, &Redis{})
assert.NoError(t, err)
// 创建一个新的测试服务器
srv := httptest.NewServer(s)
// 关闭测试服务器
defer srv.Close()
// 调用测试服务器的测试函数
testServer(srv, t)
}
这与之前的测试很相似,我们在测试开始时初始化了两个容器。
第一次启动可能需要一些时间来下载图片。但后续的启动几乎是瞬间的。
测试容器的工作原理要使用 Testcontainers 进行测试,你需要一个与 Docker API 兼容的容器运行环境,或者在本地安装 Docker。如果你尝试停止 Docker 引擎,测试将无法进行。不过,对于大多数开发人员来说,这通常不是问题,因为在 CI/CD 流水线或本地拥有一个 Docker 运行环境已经是很普遍的做法了。例如,你可以在 Github Actions 中轻松配置这样一个环境。
说到支持的语言,Testcontainers 支持多种流行的编程语言和平台,例如 Java、.NET、Go、Node.js、Python、Rust 和 Haskell。
也有一个不断增长的预配置实施列表(称为模块),你可以在这里找到这些模块:here。然而,如前所述,你可以运行任何 Docker 镜像。在 Go 中,你可以使用如下代码来部署 Redis,而不是使用预配置的模块:
// 使用现有模块
redis容器, err := redis.Run(ctx, "redis:latest")
// 或者使用 GenericContainer
req := testcontainers.ContainerRequest{
Image: "redis:latest",
ExposedPorts: []string{"6379/tcp"},
WaitingFor: wait.ForLog("Ready to accept connections"),
}
redis容器实例, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
结论
虽然集成测试的开发和维护需要投入大量精力,但它们是必不可少的,确保组件、子系统或应用程序能够良好地协同运作,是SDLC中的关键组成部分。
使用 Testcontainers,我们可以简化测试中一次性依赖项的创建和销毁,使测试运行完全隔离且更加可预测。
资源请点击这里阅读原文 使用 Testcontainers 进行集成测试
共同学习,写下你的评论
评论加载中...
作者其他优质文章