在本文中,我们将为该应用添加 API 测试。让我们开始吧!
本文内容
设置独立的测试数据库:配置一个专门用于运行测试的数据库,确保测试数据是隔离的,不会影响本地开发数据库。 实现数据工厂:引入工厂方法简化测试数据的创建。这些工厂方法可以通过一行代码生成数据库记录,从而优化测试流程。 构建测试客户端:创建一个测试客户端,用于模拟 API 请求。该客户端还支持带认证的请求,能够自动处理授权,模拟真实用户的交互。
项目已容器化,只需运行以下命令即可启动应用:
docker compose up
本文将重点介绍测试相关的内容。
测试文件结构
以下是测试文件的目录结构:
tests/
├── api_tests/ // API 控制器的测试文件
│ ├── hello_controller_test.go
│ ├── item_controller_test.go
│ └── main_test.go
├── clients/ // 测试中使用的客户端相关工具文件
│ └── api_clients.go
├── factories/ // 用于生成测试数据的工厂方法
│ ├── item_factory.go
│ └── user_factory.go
└── testutils/ // 测试中用于数据库和数据操作的工具文件
├── data_test_helper.go
└── db_test_helper.go
接下来,我们开始配置测试环境。
数据库配置与初始化
main_test.go
文件是运行项目测试的特殊入口点。
文件 main_test.go
package api_tests
import (
"testing"
"simple-gin-backend/internal/tests/testutils"
)
func TestMain(m *testing.M) {
// 使用数据库初始化测试套件
testutils.InitializeTestSuite(m)
}
TestMain
是 Go 测试框架识别的特殊函数。它接收一个 *testing.M
参数,代表测试套件,其 Run
方法负责运行所有测试。
在这个文件中,TestMain
函数在整个测试套件运行之前和之后执行一次。它用于执行全局的设置(如初始化数据库、创建测试数据)和清理逻辑(如释放资源)。在我们的例子中,它调用了 testutils.InitializeTestSuite(m)
函数来处理这些任务。
文件 db_test_helper.go
以下是 db_test_helper.go
文件的内容:
package testutils
import (
"database/sql"
"fmt"
"log"
"os"
"simple-gin-backend/internal/config"
"simple-gin-backend/internal/database"
"simple-gin-backend/internal/models"
"testing"
"gorm.io/driver/postgres"
"gorm.io/gorm"
_ "github.com/lib/pq"
)
// 创建测试数据库(如果不存在)
func createTestDB() {
// 连接默认数据库(如 postgres)以创建测试数据库
config.LoadConfig()
defaultDSN := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
config.AppConfig.PostgresHost,
config.AppConfig.PostgresUser,
config.AppConfig.PostgresPassword,
"postgres",
config.AppConfig.PostgresPort,
)
db, err := sql.Open("postgres", defaultDSN)
if err != nil {
log.Fatalf("无法连接到默认数据库: %v", err)
}
defer db.Close()
// 测试数据库名称
testDBName := config.AppConfig.PostgresDb + "_test"
// 终止所有连接到测试数据库的活动连接
_, err = db.Exec(fmt.Sprintf(`
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE pg_stat_activity.datname = '%s'
AND pid <> pg_backend_pid();
`, testDBName))
if err != nil {
log.Fatalf("无法终止测试数据库 %s 的连接: %v", testDBName, err)
}
// 如果测试数据库存在,则删除
_, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", testDBName))
if err != nil {
log.Fatalf("无法删除测试数据库 %s: %v", testDBName, err)
}
// 创建测试数据库
_, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", testDBName))
if err != nil {
log.Fatalf("无法创建测试数据库 %s: %v", testDBName, err)
} else {
log.Printf("成功创建数据库 %s", testDBName)
}
}
// 测试数据库连接
var TestDB *gorm.DB
// 设置测试数据库连接并运行迁移
func SetupTestDatabase() {
config.LoadConfig()
createTestDB()
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
config.AppConfig.PostgresHost,
config.AppConfig.PostgresUser,
config.AppConfig.PostgresPassword,
config.AppConfig.PostgresDb+"_test",
config.AppConfig.PostgresPort,
)
var err error
TestDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("无法连接到测试数据库: %v", err)
}
// 运行迁移(可以在此添加模型)
TestDB.AutoMigrate(&models.Item{}, &models.User{})
}
// 重置测试数据库
func ResetTestDatabase() {
TestDB.Exec("TRUNCATE TABLE items RESTART IDENTITY CASCADE")
}
// 关闭测试数据库连接
func TearDownTestDatabase() {
sqlDB, err := TestDB.DB()
if err != nil {
log.Fatalf("无法关闭数据库: %v", err)
}
sqlDB.Close()
}
// 替换全局数据库连接为测试数据库
func PatchDatabase() {
database.DB = TestDB
}
// 恢复原始数据库连接(如果需要)
func UnpatchDatabase() {
database.InitDB()
}
// 初始化测试套件
func InitializeTestSuite(m *testing.M) {
SetupTestDatabase()
PatchDatabase()
code := m.Run()
TearDownTestDatabase()
UnpatchDatabase()
os.Exit(code)
}
InitializeTestSuite
函数的作用
初始化测试数据库:调用 SetupTestDatabase
函数,检查测试数据库是否存在。如果不存在,则创建数据库并运行迁移。替换全局数据库连接: PatchDatabase
函数将全局的database.DB
替换为测试数据库连接。运行所有测试。 清理资源:关闭测试数据库连接。 恢复原始数据库连接。
通过这种方式,我们可以在测试中使用独立的测试数据库,而不会对本地开发数据库产生任何影响。
数据工厂
在编写测试时,我们通常需要在数据库中创建对象。为了避免在每个测试中重复创建对象的代码,我们可以创建工厂方法来简化这一过程。
用户工厂
文件 user_factory.go
:
package factories
import (
"simple-gin-backend/internal/models"
"simple-gin-backend/internal/tests/testutils"
"golang.org/x/crypto/bcrypt"
)
// 创建用户工厂
// Email: 随机生成,Password: "password"
func UserFactory() models.User {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
user := models.User{
FirstName: testutils.GenerateRandomString(10),
LastName: testutils.GenerateRandomString(10),
Email: testutils.GenerateRandomString(10) + "@example.com",
PasswordHash: string(hashedPassword),
}
testutils.TestDB.Create(&user)
testutils.TestDB.First(&user, user.ID)
return user
}
项目工厂
文件 item_factory.go
:
package factories
import (
"simple-gin-backend/internal/models"
"simple-gin-backend/internal/tests/testutils"
)
// 创建项目工厂
func ItemFactory(userID uint) models.Item {
item := models.Item{
Name: testutils.GenerateRandomString(10),
UserID: userID,
}
testutils.TestDB.Create(&item)
testutils.TestDB.First(&item, item.ID)
return item
}
测试客户端
为了测试 API 路由,我们需要创建一个专门用于测试的客户端。这个客户端需要支持带认证和不带认证的请求。
文件 api_clients.go
:
// 代码省略,保持原样
API 路由测试
以下是两个简单的测试示例:
测试无认证的路由
文件 hello_controller_test.go
:
package api_tests
import (
"net/http"
api_clients "simple-gin-backend/internal/tests/clients"
"testing"
)
func TestGetHelloWorld(t *testing.T) {
client := api_clients.NewTestClient(false)
response := client.PerformRequest("GET", "/", nil, nil)
api_clients.AssertResponse(t, response, http.StatusOK, `"Hello, world!"`)
}
测试需要认证的路由
文件 item_controller_test.go
:
// 代码省略,保持原样
运行测试
在运行的 Docker 容器中执行以下命令运行测试:
docker compose exec todo-api go test ./internal/tests/api_tests/
总结
本文介绍了如何为使用 Gin 框架和 Golang 构建的 API 应用编写测试。通过设置独立的测试数据库、实现数据工厂以及构建测试客户端,我们可以高效地编写和运行测试。将这些测试集成到 CI/CD 流程中,可以确保在代码修改、优化和重构后,应用依然能够正常运行。