基于Gin框架和Golang的应用程序API测试

科技   2024-12-03 11:03   广东  

在本文中,我们将为该应用添加 API 测试。让我们开始吧!

本文内容

  1. 设置独立的测试数据库:配置一个专门用于运行测试的数据库,确保测试数据是隔离的,不会影响本地开发数据库。
  2. 实现数据工厂:引入工厂方法简化测试数据的创建。这些工厂方法可以通过一行代码生成数据库记录,从而优化测试流程。
  3. 构建测试客户端:创建一个测试客户端,用于模拟 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 函数的作用

  1. 初始化测试数据库:调用 SetupTestDatabase 函数,检查测试数据库是否存在。如果不存在,则创建数据库并运行迁移。
  2. 替换全局数据库连接:PatchDatabase 函数将全局的 database.DB 替换为测试数据库连接。
  3. 运行所有测试。
  4. 清理资源:关闭测试数据库连接。
  5. 恢复原始数据库连接。

通过这种方式,我们可以在测试中使用独立的测试数据库,而不会对本地开发数据库产生任何影响。


数据工厂

在编写测试时,我们通常需要在数据库中创建对象。为了避免在每个测试中重复创建对象的代码,我们可以创建工厂方法来简化这一过程。

用户工厂

文件 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""/"nilnil)
 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 流程中,可以确保在代码修改、优化和重构后,应用依然能够正常运行。

点击关注并扫码添加进交流群
免费领取「Go 语言」学习资料


源自开发者
专注于提供关于Go语言的实用教程、案例分析、最新趋势,以及云原生技术的深度解析和实践经验分享。
 最新文章