feat(tests): add comprehensive unit, integration, and e2e tests for API and database functionality
- Implemented end-to-end tests for API health checks, performance, behavior, and documentation. - Created integration tests for database connection, CRUD operations, transactions, and connection pool management. - Developed unit tests for configuration loading, environment variable handling, validation, default values, and helper functions. - Established a test setup with environment management and basic usage examples for the testing framework.
This commit is contained in:
288
templates/project/tests/README.md
Normal file
288
templates/project/tests/README.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# 测试指南
|
||||
|
||||
本项目的测试使用 **Convey 框架**,分为三个层次:单元测试、集成测试和端到端测试。
|
||||
|
||||
## 测试结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── setup_test.go # 测试设置和通用工具
|
||||
├── unit/ # 单元测试
|
||||
│ ├── config_test.go # 配置测试
|
||||
│ └── ... # 其他单元测试
|
||||
├── integration/ # 集成测试
|
||||
│ ├── database_test.go # 数据库集成测试
|
||||
│ └── ... # 其他集成测试
|
||||
└── e2e/ # 端到端测试
|
||||
├── api_test.go # API 测试
|
||||
└── ... # 其他 E2E 测试
|
||||
```
|
||||
|
||||
## Convey 框架概述
|
||||
|
||||
Convey 是一个 BDD 风格的 Go 测试框架,提供直观的语法和丰富的断言。
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **Convey**: 定义测试上下文,类似于 `Describe` 或 `Context`
|
||||
- **So**: 断言函数,验证预期结果
|
||||
- **Reset**: 清理函数,在每个测试后执行
|
||||
|
||||
### 基本语法
|
||||
|
||||
```go
|
||||
Convey("测试场景描述", t, func() {
|
||||
Convey("当某个条件发生时", func() {
|
||||
// 准备测试数据
|
||||
result := SomeFunction()
|
||||
|
||||
Convey("那么应该得到预期结果", func() {
|
||||
So(result, ShouldEqual, "expected")
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
// 清理测试数据
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有测试
|
||||
```bash
|
||||
go test ./tests/... -v
|
||||
```
|
||||
|
||||
### 运行特定类型的测试
|
||||
```bash
|
||||
# 单元测试
|
||||
go test ./tests/unit/... -v
|
||||
|
||||
# 集成测试
|
||||
go test ./tests/integration/... -v
|
||||
|
||||
# 端到端测试
|
||||
go test ./tests/e2e/... -v
|
||||
```
|
||||
|
||||
### 运行带覆盖率报告的测试
|
||||
```bash
|
||||
go test ./tests/... -v -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
```
|
||||
|
||||
### 运行基准测试
|
||||
```bash
|
||||
go test ./tests/... -bench=. -v
|
||||
```
|
||||
|
||||
## 测试环境配置
|
||||
|
||||
### 单元测试
|
||||
- 不需要外部依赖
|
||||
- 使用内存数据库或模拟对象
|
||||
- 快速执行
|
||||
|
||||
### 集成测试
|
||||
- 需要数据库连接
|
||||
- 使用测试数据库 `{{.ProjectName}}_test`
|
||||
- 需要启动 Redis 等服务
|
||||
|
||||
### 端到端测试
|
||||
- 需要完整的应用环境
|
||||
- 测试真实的 HTTP 请求
|
||||
- 可能需要 Docker 环境
|
||||
|
||||
## Convey 测试最佳实践
|
||||
|
||||
### 1. 测试结构设计
|
||||
- 使用描述性的中文场景描述
|
||||
- 遵循 `当...那么...` 的语义结构
|
||||
- 嵌套 Convey 块来组织复杂测试逻辑
|
||||
|
||||
```go
|
||||
Convey("用户认证测试", t, func() {
|
||||
var user *User
|
||||
var token string
|
||||
|
||||
Convey("当用户注册时", func() {
|
||||
user = &User{Name: "测试用户", Email: "test@example.com"}
|
||||
err := user.Register()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("那么用户应该被创建", func() {
|
||||
So(user.ID, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当用户登录时", func() {
|
||||
token, err := user.Login("password")
|
||||
So(err, ShouldBeNil)
|
||||
So(token, ShouldNotBeEmpty)
|
||||
|
||||
Convey("那么应该获得有效的访问令牌", func() {
|
||||
So(len(token), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 断言使用
|
||||
- 使用丰富的 So 断言函数
|
||||
- 提供有意义的错误消息
|
||||
- 验证所有重要的方面
|
||||
|
||||
### 3. 数据管理
|
||||
- 使用 `Reset` 函数进行清理
|
||||
- 每个测试独立准备数据
|
||||
- 确保测试间不相互影响
|
||||
|
||||
### 4. 异步测试
|
||||
- 使用适当的超时设置
|
||||
- 处理并发测试
|
||||
- 使用 channel 进行同步
|
||||
|
||||
### 5. 错误处理
|
||||
- 测试错误情况
|
||||
- 验证错误消息
|
||||
- 确保错误处理逻辑正确
|
||||
|
||||
## 常用 Convey 断言
|
||||
|
||||
### 相等性断言
|
||||
```go
|
||||
So(value, ShouldEqual, expected)
|
||||
So(value, ShouldNotEqual, expected)
|
||||
So(value, ShouldResemble, expected) // 深度比较
|
||||
So(value, ShouldNotResemble, expected)
|
||||
```
|
||||
|
||||
### 类型断言
|
||||
```go
|
||||
So(value, ShouldBeNil)
|
||||
So(value, ShouldNotBeNil)
|
||||
So(value, ShouldBeTrue)
|
||||
So(value, ShouldBeFalse)
|
||||
So(value, ShouldBeZeroValue)
|
||||
```
|
||||
|
||||
### 数值断言
|
||||
```go
|
||||
So(value, ShouldBeGreaterThan, expected)
|
||||
So(value, ShouldBeLessThan, expected)
|
||||
So(value, ShouldBeBetween, lower, upper)
|
||||
```
|
||||
|
||||
### 集合断言
|
||||
```go
|
||||
So(slice, ShouldHaveLength, expected)
|
||||
So(slice, ShouldContain, expected)
|
||||
So(slice, ShouldNotContain, expected)
|
||||
So(map, ShouldContainKey, key)
|
||||
```
|
||||
|
||||
### 字符串断言
|
||||
```go
|
||||
So(str, ShouldContainSubstring, substr)
|
||||
So(str, ShouldStartWith, prefix)
|
||||
So(str, ShouldEndWith, suffix)
|
||||
So(str, ShouldMatch, regexp)
|
||||
```
|
||||
|
||||
### 错误断言
|
||||
```go
|
||||
So(err, ShouldBeNil)
|
||||
So(err, ShouldNotBeNil)
|
||||
So(err, ShouldError, expectedError)
|
||||
```
|
||||
|
||||
## 测试工具
|
||||
|
||||
- `goconvey/convey` - BDD 测试框架
|
||||
- `gomock` - Mock 生成器
|
||||
- `httptest` - HTTP 测试
|
||||
- `sqlmock` - 数据库 mock
|
||||
- `testify` - 辅助测试工具(可选)
|
||||
|
||||
## 测试示例
|
||||
|
||||
### 配置测试示例
|
||||
```go
|
||||
Convey("配置加载测试", t, func() {
|
||||
var config *Config
|
||||
|
||||
Convey("当从文件加载配置时", func() {
|
||||
config, err := LoadConfig("config.toml")
|
||||
So(err, ShouldBeNil)
|
||||
So(config, ShouldNotBeNil)
|
||||
|
||||
Convey("那么配置应该正确加载", func() {
|
||||
So(config.App.Mode, ShouldEqual, "development")
|
||||
So(config.Http.Port, ShouldEqual, 8080)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 数据库测试示例
|
||||
```go
|
||||
Convey("数据库操作测试", t, func() {
|
||||
var db *gorm.DB
|
||||
|
||||
Convey("当连接数据库时", func() {
|
||||
db = SetupTestDB()
|
||||
So(db, ShouldNotBeNil)
|
||||
|
||||
Convey("那么应该能够创建记录", func() {
|
||||
user := User{Name: "测试用户", Email: "test@example.com"}
|
||||
result := db.Create(&user)
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(user.ID, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if db != nil {
|
||||
CleanupTestDB(db)
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### API 测试示例
|
||||
```go
|
||||
Convey("API 端点测试", t, func() {
|
||||
var server *httptest.Server
|
||||
|
||||
Convey("当启动测试服务器时", func() {
|
||||
server = httptest.NewServer(NewApp())
|
||||
So(server, ShouldNotBeNil)
|
||||
|
||||
Convey("那么健康检查端点应该正常工作", func() {
|
||||
resp, err := http.Get(server.URL + "/health")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
So(result["status"], ShouldEqual, "ok")
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if server != nil {
|
||||
server.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## CI/CD 集成
|
||||
|
||||
测试会在以下情况下自动运行:
|
||||
- 代码提交时
|
||||
- 创建 Pull Request 时
|
||||
- 合并到主分支时
|
||||
|
||||
测试结果会影响代码合并决策。Convey 的详细输出有助于快速定位问题。
|
||||
419
templates/project/tests/e2e/api_test.go
Normal file
419
templates/project/tests/e2e/api_test.go
Normal file
@@ -0,0 +1,419 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"{{.ModuleName}}/app"
|
||||
"{{.ModuleName}}/app/config"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
// TestAPIHealth 测试 API 健康检查
|
||||
func TestAPIHealth(t *testing.T) {
|
||||
Convey("API 健康检查测试", t, func() {
|
||||
var server *httptest.Server
|
||||
var testConfig *config.Config
|
||||
|
||||
Convey("当启动测试服务器时", func() {
|
||||
testConfig = &config.Config{
|
||||
App: config.AppConfig{
|
||||
Mode: "test",
|
||||
BaseURI: "http://localhost:8080",
|
||||
},
|
||||
Http: config.HttpConfig{
|
||||
Port: 8080,
|
||||
},
|
||||
Log: config.LogConfig{
|
||||
Level: "debug",
|
||||
Format: "text",
|
||||
EnableCaller: true,
|
||||
},
|
||||
}
|
||||
|
||||
app := app.New(testConfig)
|
||||
server = httptest.NewServer(app)
|
||||
|
||||
Convey("服务器应该成功启动", func() {
|
||||
So(server, ShouldNotBeNil)
|
||||
So(server.URL, ShouldNotBeEmpty)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当访问健康检查端点时", func() {
|
||||
resp, err := http.Get(server.URL + "/health")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("响应应该包含正确的状态", func() {
|
||||
So(result["status"], ShouldEqual, "ok")
|
||||
})
|
||||
|
||||
Convey("响应应该包含时间戳", func() {
|
||||
So(result, ShouldContainKey, "timestamp")
|
||||
})
|
||||
|
||||
Convey("响应应该是 JSON 格式", func() {
|
||||
So(resp.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当访问不存在的端点时", func() {
|
||||
resp, err := http.Get(server.URL + "/api/nonexistent")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("响应应该包含错误信息", func() {
|
||||
So(result, ShouldContainKey, "error")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试 CORS 支持", func() {
|
||||
req, err := http.NewRequest("OPTIONS", server.URL+"/api/test", nil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
req.Header.Set("Origin", "http://localhost:3000")
|
||||
req.Header.Set("Access-Control-Request-Method", "POST")
|
||||
req.Header.Set("Access-Control-Request-Headers", "Content-Type,Authorization")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
Convey("应该返回正确的 CORS 头", func() {
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
So(resp.Header.Get("Access-Control-Allow-Origin"), ShouldContainSubstring, "localhost")
|
||||
So(resp.Header.Get("Access-Control-Allow-Methods"), ShouldContainSubstring, "POST")
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if server != nil {
|
||||
server.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIPerformance 测试 API 性能
|
||||
func TestAPIPerformance(t *testing.T) {
|
||||
Convey("API 性能测试", t, func() {
|
||||
var server *httptest.Server
|
||||
var testConfig *config.Config
|
||||
|
||||
Convey("当准备性能测试时", func() {
|
||||
testConfig = &config.Config{
|
||||
App: config.AppConfig{
|
||||
Mode: "test",
|
||||
BaseURI: "http://localhost:8080",
|
||||
},
|
||||
Http: config.HttpConfig{
|
||||
Port: 8080,
|
||||
},
|
||||
Log: config.LogConfig{
|
||||
Level: "error", // 减少日志输出以提升性能
|
||||
Format: "text",
|
||||
},
|
||||
}
|
||||
|
||||
app := app.New(testConfig)
|
||||
server = httptest.NewServer(app)
|
||||
})
|
||||
|
||||
Convey("当测试响应时间时", func() {
|
||||
start := time.Now()
|
||||
resp, err := http.Get(server.URL + "/health")
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
duration := time.Since(start)
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
|
||||
Convey("响应时间应该在合理范围内", func() {
|
||||
So(duration, ShouldBeLessThan, 100*time.Millisecond)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试并发请求时", func() {
|
||||
const numRequests = 50
|
||||
const maxConcurrency = 10
|
||||
const timeout = 5 * time.Second
|
||||
|
||||
var wg sync.WaitGroup
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
var mu sync.Mutex
|
||||
|
||||
// 使用信号量控制并发数
|
||||
sem := make(chan struct{}, maxConcurrency)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
wg.Add(1)
|
||||
go func(requestID int) {
|
||||
defer wg.Done()
|
||||
|
||||
// 获取信号量
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", server.URL+"/health", nil)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
errorCount++
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
errorCount++
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
mu.Lock()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
successCount++
|
||||
} else {
|
||||
errorCount++
|
||||
}
|
||||
mu.Unlock()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
Convey("所有请求都应该完成", func() {
|
||||
So(successCount+errorCount, ShouldEqual, numRequests)
|
||||
})
|
||||
|
||||
Convey("所有请求都应该成功", func() {
|
||||
So(errorCount, ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("总耗时应该在合理范围内", func() {
|
||||
So(duration, ShouldBeLessThan, 10*time.Second)
|
||||
})
|
||||
|
||||
Convey("并发性能应该良好", func() {
|
||||
avgTime := duration / numRequests
|
||||
So(avgTime, ShouldBeLessThan, 200*time.Millisecond)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if server != nil {
|
||||
server.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIBehavior 测试 API 行为
|
||||
func TestAPIBehavior(t *testing.T) {
|
||||
Convey("API 行为测试", t, func() {
|
||||
var server *httptest.Server
|
||||
var testConfig *config.Config
|
||||
|
||||
Convey("当准备行为测试时", func() {
|
||||
testConfig = &config.Config{
|
||||
App: config.AppConfig{
|
||||
Mode: "test",
|
||||
BaseURI: "http://localhost:8080",
|
||||
},
|
||||
Http: config.HttpConfig{
|
||||
Port: 8080,
|
||||
},
|
||||
Log: config.LogConfig{
|
||||
Level: "debug",
|
||||
Format: "text",
|
||||
EnableCaller: true,
|
||||
},
|
||||
}
|
||||
|
||||
app := app.New(testConfig)
|
||||
server = httptest.NewServer(app)
|
||||
})
|
||||
|
||||
Convey("当测试不同 HTTP 方法时", func() {
|
||||
testURL := server.URL + "/health"
|
||||
|
||||
Convey("GET 请求应该成功", func() {
|
||||
resp, err := http.Get(testURL)
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
Convey("POST 请求应该被处理", func() {
|
||||
resp, err := http.Post(testURL, "application/json", bytes.NewBuffer([]byte{}))
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
// 健康检查端点通常支持所有方法
|
||||
So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed})
|
||||
})
|
||||
|
||||
Convey("PUT 请求应该被处理", func() {
|
||||
req, err := http.NewRequest("PUT", testURL, bytes.NewBuffer([]byte{}))
|
||||
So(err, ShouldBeNil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed})
|
||||
})
|
||||
|
||||
Convey("DELETE 请求应该被处理", func() {
|
||||
req, err := http.NewRequest("DELETE", testURL, nil)
|
||||
So(err, ShouldBeNil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusMethodNotAllowed})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试自定义请求头时", func() {
|
||||
req, err := http.NewRequest("GET", server.URL+"/health", nil)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// 设置各种请求头
|
||||
req.Header.Set("User-Agent", "E2E-Test-Agent/1.0")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
req.Header.Set("X-Custom-Header", "test-value")
|
||||
req.Header.Set("X-Request-ID", "test-request-123")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
Convey("请求应该成功", func() {
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
Convey("响应应该是 JSON 格式", func() {
|
||||
So(resp.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试错误处理时", func() {
|
||||
Convey("访问不存在的路径应该返回 404", func() {
|
||||
resp, err := http.Get(server.URL + "/api/v1/nonexistent")
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
|
||||
Convey("访问非法路径应该返回 404", func() {
|
||||
resp, err := http.Get(server.URL + "/../etc/passwd")
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if server != nil {
|
||||
server.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIDocumentation 测试 API 文档
|
||||
func TestAPIDocumentation(t *testing.T) {
|
||||
Convey("API 文档测试", t, func() {
|
||||
var server *httptest.Server
|
||||
var testConfig *config.Config
|
||||
|
||||
Convey("当准备文档测试时", func() {
|
||||
testConfig = &config.Config{
|
||||
App: config.AppConfig{
|
||||
Mode: "test",
|
||||
BaseURI: "http://localhost:8080",
|
||||
},
|
||||
Http: config.HttpConfig{
|
||||
Port: 8080,
|
||||
},
|
||||
Log: config.LogConfig{
|
||||
Level: "debug",
|
||||
Format: "text",
|
||||
EnableCaller: true,
|
||||
},
|
||||
}
|
||||
|
||||
app := app.New(testConfig)
|
||||
server = httptest.NewServer(app)
|
||||
})
|
||||
|
||||
Convey("当访问 Swagger UI 时", func() {
|
||||
resp, err := http.Get(server.URL + "/swagger/index.html")
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
Convey("应该能够访问 Swagger UI", func() {
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
})
|
||||
|
||||
Convey("响应应该是 HTML 格式", func() {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
So(contentType, ShouldContainSubstring, "text/html")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当访问 OpenAPI 规范时", func() {
|
||||
resp, err := http.Get(server.URL + "/swagger/doc.json")
|
||||
So(err, ShouldBeNil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
Convey("应该能够访问 OpenAPI 规范", func() {
|
||||
// 如果存在则返回 200,不存在则返回 404
|
||||
So(resp.StatusCode, ShouldBeIn, []int{http.StatusOK, http.StatusNotFound})
|
||||
})
|
||||
|
||||
Convey("如果存在,响应应该是 JSON 格式", func() {
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
So(contentType, ShouldContainSubstring, "application/json")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if server != nil {
|
||||
server.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
364
templates/project/tests/integration/database_test.go
Normal file
364
templates/project/tests/integration/database_test.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"{{.ModuleName}}/app/config"
|
||||
"{{.ModuleName}}/app/database"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestUser 测试用户模型
|
||||
type TestUser struct {
|
||||
ID int `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100;not null"`
|
||||
Email string `gorm:"size:100;unique;not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
}
|
||||
|
||||
// TestDatabaseConnection 测试数据库连接
|
||||
func TestDatabaseConnection(t *testing.T) {
|
||||
Convey("数据库连接测试", t, func() {
|
||||
var db *gorm.DB
|
||||
var sqlDB *sql.DB
|
||||
var testConfig *config.Config
|
||||
var testDBName string
|
||||
|
||||
Convey("当准备测试数据库时", func() {
|
||||
testDBName = "{{.ProjectName}}_test_integration"
|
||||
testConfig = &config.Config{
|
||||
Database: config.DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: testDBName,
|
||||
Username: "postgres",
|
||||
Password: "password",
|
||||
SslMode: "disable",
|
||||
MaxIdleConns: 5,
|
||||
MaxOpenConns: 20,
|
||||
ConnMaxLifetime: 30 * time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
Convey("应该能够连接到数据库", func() {
|
||||
dsn := testConfig.Database.GetDSN()
|
||||
var err error
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
So(err, ShouldBeNil)
|
||||
So(db, ShouldNotBeNil)
|
||||
|
||||
sqlDB, err = db.DB()
|
||||
So(err, ShouldBeNil)
|
||||
So(sqlDB, ShouldNotBeNil)
|
||||
|
||||
// 设置连接池
|
||||
sqlDB.SetMaxIdleConns(testConfig.Database.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(testConfig.Database.MaxOpenConns)
|
||||
sqlDB.SetConnMaxLifetime(testConfig.Database.ConnMaxLifetime)
|
||||
|
||||
// 测试连接
|
||||
err = sqlDB.Ping()
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("应该能够创建测试表", func() {
|
||||
err := db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS integration_test_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`).Error
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试数据库操作时", func() {
|
||||
Convey("应该能够创建记录", func() {
|
||||
user := TestUser{
|
||||
Name: "Integration Test User",
|
||||
Email: "integration@example.com",
|
||||
}
|
||||
|
||||
result := db.Create(&user)
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(result.RowsAffected, ShouldEqual, 1)
|
||||
So(user.ID, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("应该能够查询记录", func() {
|
||||
// 先插入测试数据
|
||||
user := TestUser{
|
||||
Name: "Query Test User",
|
||||
Email: "query@example.com",
|
||||
}
|
||||
db.Create(&user)
|
||||
|
||||
// 查询记录
|
||||
var result TestUser
|
||||
err := db.First(&result, "email = ?", "query@example.com").Error
|
||||
So(err, ShouldBeNil)
|
||||
So(result.Name, ShouldEqual, "Query Test User")
|
||||
So(result.Email, ShouldEqual, "query@example.com")
|
||||
})
|
||||
|
||||
Convey("应该能够更新记录", func() {
|
||||
// 先插入测试数据
|
||||
user := TestUser{
|
||||
Name: "Update Test User",
|
||||
Email: "update@example.com",
|
||||
}
|
||||
db.Create(&user)
|
||||
|
||||
// 更新记录
|
||||
result := db.Model(&user).Update("name", "Updated Integration User")
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(result.RowsAffected, ShouldEqual, 1)
|
||||
|
||||
// 验证更新
|
||||
var updatedUser TestUser
|
||||
err := db.First(&updatedUser, user.ID).Error
|
||||
So(err, ShouldBeNil)
|
||||
So(updatedUser.Name, ShouldEqual, "Updated Integration User")
|
||||
})
|
||||
|
||||
Convey("应该能够删除记录", func() {
|
||||
// 先插入测试数据
|
||||
user := TestUser{
|
||||
Name: "Delete Test User",
|
||||
Email: "delete@example.com",
|
||||
}
|
||||
db.Create(&user)
|
||||
|
||||
// 删除记录
|
||||
result := db.Delete(&user)
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(result.RowsAffected, ShouldEqual, 1)
|
||||
|
||||
// 验证删除
|
||||
var deletedUser TestUser
|
||||
err := db.First(&deletedUser, user.ID).Error
|
||||
So(err, ShouldEqual, gorm.ErrRecordNotFound)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试事务时", func() {
|
||||
Convey("应该能够执行事务操作", func() {
|
||||
// 开始事务
|
||||
tx := db.Begin()
|
||||
So(tx, ShouldNotBeNil)
|
||||
|
||||
// 在事务中插入数据
|
||||
user := TestUser{
|
||||
Name: "Transaction Test User",
|
||||
Email: "transaction@example.com",
|
||||
}
|
||||
result := tx.Create(&user)
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(result.RowsAffected, ShouldEqual, 1)
|
||||
|
||||
// 查询事务中的数据
|
||||
var count int64
|
||||
tx.Model(&TestUser{}).Count(&count)
|
||||
So(count, ShouldEqual, 1)
|
||||
|
||||
// 提交事务
|
||||
err := tx.Commit().Error
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// 验证数据已提交
|
||||
db.Model(&TestUser{}).Count(&count)
|
||||
So(count, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("应该能够回滚事务", func() {
|
||||
// 开始事务
|
||||
tx := db.Begin()
|
||||
|
||||
// 在事务中插入数据
|
||||
user := TestUser{
|
||||
Name: "Rollback Test User",
|
||||
Email: "rollback@example.com",
|
||||
}
|
||||
tx.Create(&user)
|
||||
|
||||
// 回滚事务
|
||||
err := tx.Rollback().Error
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// 验证数据已回滚
|
||||
var count int64
|
||||
db.Model(&TestUser{}).Where("email = ?", "rollback@example.com").Count(&count)
|
||||
So(count, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试批量操作时", func() {
|
||||
Convey("应该能够批量插入记录", func() {
|
||||
users := []TestUser{
|
||||
{Name: "Batch User 1", Email: "batch1@example.com"},
|
||||
{Name: "Batch User 2", Email: "batch2@example.com"},
|
||||
{Name: "Batch User 3", Email: "batch3@example.com"},
|
||||
}
|
||||
|
||||
result := db.Create(&users)
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(result.RowsAffected, ShouldEqual, 3)
|
||||
|
||||
// 验证批量插入
|
||||
var count int64
|
||||
db.Model(&TestUser{}).Where("email LIKE ?", "batch%@example.com").Count(&count)
|
||||
So(count, ShouldEqual, 3)
|
||||
})
|
||||
|
||||
Convey("应该能够批量更新记录", func() {
|
||||
// 先插入测试数据
|
||||
users := []TestUser{
|
||||
{Name: "Batch Update 1", Email: "batchupdate1@example.com"},
|
||||
{Name: "Batch Update 2", Email: "batchupdate2@example.com"},
|
||||
}
|
||||
db.Create(&users)
|
||||
|
||||
// 批量更新
|
||||
result := db.Model(&TestUser{}).
|
||||
Where("email LIKE ?", "batchupdate%@example.com").
|
||||
Update("name", "Batch Updated User")
|
||||
So(result.Error, ShouldBeNil)
|
||||
So(result.RowsAffected, ShouldEqual, 2)
|
||||
|
||||
// 验证更新
|
||||
var updatedCount int64
|
||||
db.Model(&TestUser{}).
|
||||
Where("name = ?", "Batch Updated User").
|
||||
Count(&updatedCount)
|
||||
So(updatedCount, ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当测试查询条件时", func() {
|
||||
Convey("应该能够使用各种查询条件", func() {
|
||||
// 插入测试数据
|
||||
testUsers := []TestUser{
|
||||
{Name: "Alice", Email: "alice@example.com"},
|
||||
{Name: "Bob", Email: "bob@example.com"},
|
||||
{Name: "Charlie", Email: "charlie@example.com"},
|
||||
{Name: "Alice Smith", Email: "alice.smith@example.com"},
|
||||
}
|
||||
db.Create(&testUsers)
|
||||
|
||||
Convey("应该能够使用 LIKE 查询", func() {
|
||||
var users []TestUser
|
||||
err := db.Where("name LIKE ?", "Alice%").Find(&users).Error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(users), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("应该能够使用 IN 查询", func() {
|
||||
var users []TestUser
|
||||
err := db.Where("name IN ?", []string{"Alice", "Bob"}).Find(&users).Error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(users), ShouldEqual, 2)
|
||||
})
|
||||
|
||||
Convey("应该能够使用 BETWEEN 查询", func() {
|
||||
var users []TestUser
|
||||
err := db.Where("id BETWEEN ? AND ?", 1, 3).Find(&users).Error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(users), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
|
||||
Convey("应该能够使用多条件查询", func() {
|
||||
var users []TestUser
|
||||
err := db.Where("name LIKE ? AND email LIKE ?", "%Alice%", "%example.com").Find(&users).Error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(users), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
// 清理测试表
|
||||
if db != nil {
|
||||
db.Exec("DROP TABLE IF EXISTS integration_test_users")
|
||||
}
|
||||
// 关闭数据库连接
|
||||
if sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestDatabaseConnectionPool 测试数据库连接池
|
||||
func TestDatabaseConnectionPool(t *testing.T) {
|
||||
Convey("数据库连接池测试", t, func() {
|
||||
var db *gorm.DB
|
||||
var sqlDB *sql.DB
|
||||
|
||||
Convey("当配置连接池时", func() {
|
||||
testConfig := &config.Config{
|
||||
Database: config.DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: "{{.ProjectName}}_test_pool",
|
||||
Username: "postgres",
|
||||
Password: "password",
|
||||
SslMode: "disable",
|
||||
MaxIdleConns: 5,
|
||||
MaxOpenConns: 10,
|
||||
ConnMaxLifetime: 5 * time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
dsn := testConfig.Database.GetDSN()
|
||||
var err error
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
sqlDB, err = db.DB()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("应该能够设置连接池参数", func() {
|
||||
sqlDB.SetMaxIdleConns(testConfig.Database.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(testConfig.Database.MaxOpenConns)
|
||||
sqlDB.SetConnMaxLifetime(testConfig.Database.ConnMaxLifetime)
|
||||
|
||||
// 验证设置
|
||||
stats := sqlDB.Stats()
|
||||
So(stats.MaxOpenConns, ShouldEqual, testConfig.Database.MaxOpenConns)
|
||||
So(stats.MaxIdleConns, ShouldEqual, testConfig.Database.MaxIdleConns)
|
||||
})
|
||||
|
||||
Convey("应该能够监控连接池状态", func() {
|
||||
// 获取初始状态
|
||||
initialStats := sqlDB.Stats()
|
||||
So(initialStats.OpenConnections, ShouldEqual, 0)
|
||||
|
||||
// 执行一些查询来创建连接
|
||||
for i := 0; i < 3; i++ {
|
||||
sqlDB.Ping()
|
||||
}
|
||||
|
||||
// 获取使用后的状态
|
||||
afterStats := sqlDB.Stats()
|
||||
So(afterStats.OpenConnections, ShouldBeGreaterThan, 0)
|
||||
So(afterStats.InUse, ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
// 关闭数据库连接
|
||||
if sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
161
templates/project/tests/setup_test.go
Normal file
161
templates/project/tests/setup_test.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
// TestMain 测试入口点
|
||||
func TestMain(m *testing.M) {
|
||||
// 运行测试
|
||||
m.Run()
|
||||
}
|
||||
|
||||
// TestSetup 测试基础设置
|
||||
func TestSetup(t *testing.T) {
|
||||
Convey("测试基础设置", t, func() {
|
||||
Convey("当初始化测试环境时", func() {
|
||||
// 初始化测试环境
|
||||
testEnv := &TestEnvironment{
|
||||
Name: "test-env",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
Convey("那么测试环境应该被正确创建", func() {
|
||||
So(testEnv.Name, ShouldEqual, "test-env")
|
||||
So(testEnv.Version, ShouldEqual, "1.0.0")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnvironment 测试环境结构
|
||||
type TestEnvironment struct {
|
||||
Name string
|
||||
Version string
|
||||
Config map[string]interface{}
|
||||
}
|
||||
|
||||
// NewTestEnvironment 创建新的测试环境
|
||||
func NewTestEnvironment(name string) *TestEnvironment {
|
||||
return &TestEnvironment{
|
||||
Name: name,
|
||||
Config: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithConfig 设置配置
|
||||
func (e *TestEnvironment) WithConfig(key string, value interface{}) *TestEnvironment {
|
||||
e.Config[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (e *TestEnvironment) GetConfig(key string) interface{} {
|
||||
return e.Config[key]
|
||||
}
|
||||
|
||||
// Setup 设置测试环境
|
||||
func (e *TestEnvironment) Setup() *TestEnvironment {
|
||||
// 初始化测试环境
|
||||
e.Config["initialized"] = true
|
||||
return e
|
||||
}
|
||||
|
||||
// Cleanup 清理测试环境
|
||||
func (e *TestEnvironment) Cleanup() {
|
||||
// 清理测试环境
|
||||
e.Config = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// TestEnvironmentManagement 测试环境管理
|
||||
func TestEnvironmentManagement(t *testing.T) {
|
||||
Convey("测试环境管理", t, func() {
|
||||
var env *TestEnvironment
|
||||
|
||||
Convey("当创建新测试环境时", func() {
|
||||
env = NewTestEnvironment("test-app")
|
||||
|
||||
Convey("那么环境应该有正确的名称", func() {
|
||||
So(env.Name, ShouldEqual, "test-app")
|
||||
So(env.Version, ShouldBeEmpty)
|
||||
So(env.Config, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当设置配置时", func() {
|
||||
env.WithConfig("debug", true)
|
||||
env.WithConfig("port", 8080)
|
||||
|
||||
Convey("那么配置应该被正确设置", func() {
|
||||
So(env.GetConfig("debug"), ShouldEqual, true)
|
||||
So(env.GetConfig("port"), ShouldEqual, 8080)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当初始化环境时", func() {
|
||||
env.Setup()
|
||||
|
||||
Convey("那么环境应该被标记为已初始化", func() {
|
||||
So(env.GetConfig("initialized"), ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
if env != nil {
|
||||
env.Cleanup()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestConveyBasicUsage 测试 Convey 基础用法
|
||||
func TestConveyBasicUsage(t *testing.T) {
|
||||
Convey("Convey 基础用法测试", t, func() {
|
||||
Convey("数字操作", func() {
|
||||
num := 42
|
||||
|
||||
Convey("应该能够进行基本比较", func() {
|
||||
So(num, ShouldEqual, 42)
|
||||
So(num, ShouldBeGreaterThan, 0)
|
||||
So(num, ShouldBeLessThan, 100)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("字符串操作", func() {
|
||||
str := "hello world"
|
||||
|
||||
Convey("应该能够进行字符串比较", func() {
|
||||
So(str, ShouldEqual, "hello world")
|
||||
So(str, ShouldContainSubstring, "hello")
|
||||
So(str, ShouldStartWith, "hello")
|
||||
So(str, ShouldEndWith, "world")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("切片操作", func() {
|
||||
slice := []int{1, 2, 3, 4, 5}
|
||||
|
||||
Convey("应该能够进行切片操作", func() {
|
||||
So(slice, ShouldHaveLength, 5)
|
||||
So(slice, ShouldContain, 3)
|
||||
So(slice, ShouldNotContain, 6)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Map 操作", func() {
|
||||
m := map[string]interface{}{
|
||||
"name": "test",
|
||||
"value": 123,
|
||||
}
|
||||
|
||||
Convey("应该能够进行 Map 操作", func() {
|
||||
So(m, ShouldContainKey, "name")
|
||||
So(m, ShouldContainKey, "value")
|
||||
So(m["name"], ShouldEqual, "test")
|
||||
So(m["value"], ShouldEqual, 123)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
287
templates/project/tests/unit/config_test.go
Normal file
287
templates/project/tests/unit/config_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"{{.ModuleName}}/app/config"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
// TestConfigLoading 测试配置加载功能
|
||||
func TestConfigLoading(t *testing.T) {
|
||||
Convey("配置加载测试", t, func() {
|
||||
var testConfig *config.Config
|
||||
var configPath string
|
||||
var testDir string
|
||||
|
||||
Convey("当准备测试配置文件时", func() {
|
||||
originalWd, _ := os.Getwd()
|
||||
testDir = filepath.Join(originalWd, "..", "..", "fixtures", "test_config")
|
||||
|
||||
Convey("应该创建测试配置目录", func() {
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("应该创建测试配置文件", func() {
|
||||
testConfigContent := `App:
|
||||
Mode: "test"
|
||||
BaseURI: "http://localhost:8080"
|
||||
Http:
|
||||
Port: 8080
|
||||
Database:
|
||||
Host: "localhost"
|
||||
Port: 5432
|
||||
Database: "test_db"
|
||||
Username: "test_user"
|
||||
Password: "test_password"
|
||||
SslMode: "disable"
|
||||
Log:
|
||||
Level: "debug"
|
||||
Format: "text"
|
||||
EnableCaller: true`
|
||||
|
||||
configPath = filepath.Join(testDir, "config.toml")
|
||||
err := os.WriteFile(configPath, []byte(testConfigContent), 0644)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("应该成功加载配置", func() {
|
||||
var err error
|
||||
testConfig, err = config.Load(configPath)
|
||||
So(err, ShouldBeNil)
|
||||
So(testConfig, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("验证配置内容", func() {
|
||||
So(testConfig, ShouldNotBeNil)
|
||||
|
||||
Convey("应用配置应该正确", func() {
|
||||
So(testConfig.App.Mode, ShouldEqual, "test")
|
||||
So(testConfig.App.BaseURI, ShouldEqual, "http://localhost:8080")
|
||||
})
|
||||
|
||||
Convey("HTTP配置应该正确", func() {
|
||||
So(testConfig.Http.Port, ShouldEqual, 8080)
|
||||
})
|
||||
|
||||
Convey("数据库配置应该正确", func() {
|
||||
So(testConfig.Database.Host, ShouldEqual, "localhost")
|
||||
So(testConfig.Database.Port, ShouldEqual, 5432)
|
||||
So(testConfig.Database.Database, ShouldEqual, "test_db")
|
||||
So(testConfig.Database.Username, ShouldEqual, "test_user")
|
||||
So(testConfig.Database.Password, ShouldEqual, "test_password")
|
||||
So(testConfig.Database.SslMode, ShouldEqual, "disable")
|
||||
})
|
||||
|
||||
Convey("日志配置应该正确", func() {
|
||||
So(testConfig.Log.Level, ShouldEqual, "debug")
|
||||
So(testConfig.Log.Format, ShouldEqual, "text")
|
||||
So(testConfig.Log.EnableCaller, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
// 清理测试文件
|
||||
if testDir != "" {
|
||||
os.RemoveAll(testDir)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestConfigFromEnvironment 测试从环境变量加载配置
|
||||
func TestConfigFromEnvironment(t *testing.T) {
|
||||
Convey("环境变量配置测试", t, func() {
|
||||
var originalEnvVars map[string]string
|
||||
|
||||
Convey("当设置环境变量时", func() {
|
||||
// 保存原始环境变量
|
||||
originalEnvVars = map[string]string{
|
||||
"APP_MODE": os.Getenv("APP_MODE"),
|
||||
"HTTP_PORT": os.Getenv("HTTP_PORT"),
|
||||
"DB_HOST": os.Getenv("DB_HOST"),
|
||||
}
|
||||
|
||||
// 设置测试环境变量
|
||||
os.Setenv("APP_MODE", "test")
|
||||
os.Setenv("HTTP_PORT", "9090")
|
||||
os.Setenv("DB_HOST", "test-host")
|
||||
|
||||
Convey("环境变量应该被正确设置", func() {
|
||||
So(os.Getenv("APP_MODE"), ShouldEqual, "test")
|
||||
So(os.Getenv("HTTP_PORT"), ShouldEqual, "9090")
|
||||
So(os.Getenv("DB_HOST"), ShouldEqual, "test-host")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当从环境变量加载配置时", func() {
|
||||
originalWd, _ := os.Getwd()
|
||||
testDir := filepath.Join(originalWd, "..", "..", "fixtures", "test_config_env")
|
||||
|
||||
Convey("应该创建测试配置目录", func() {
|
||||
err := os.MkdirAll(testDir, 0755)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("应该创建基础配置文件", func() {
|
||||
testConfigContent := `App:
|
||||
Mode: "development"
|
||||
BaseURI: "http://localhost:3000"
|
||||
Http:
|
||||
Port: 3000
|
||||
Database:
|
||||
Host: "localhost"
|
||||
Port: 5432
|
||||
Database: "default_db"
|
||||
Username: "default_user"
|
||||
Password: "default_password"
|
||||
SslMode: "disable"`
|
||||
|
||||
configPath := filepath.Join(testDir, "config.toml")
|
||||
err := os.WriteFile(configPath, []byte(testConfigContent), 0644)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("应该成功加载并合并配置", func() {
|
||||
configPath := filepath.Join(testDir, "config.toml")
|
||||
loadedConfig, err := config.Load(configPath)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(loadedConfig, ShouldNotBeNil)
|
||||
|
||||
Convey("环境变量应该覆盖配置文件", func() {
|
||||
So(loadedConfig.App.Mode, ShouldEqual, "test")
|
||||
So(loadedConfig.Http.Port, ShouldEqual, 9090)
|
||||
So(loadedConfig.Database.Host, ShouldEqual, "test-host")
|
||||
})
|
||||
|
||||
Convey("配置文件的默认值应该保留", func() {
|
||||
So(loadedConfig.App.BaseURI, ShouldEqual, "http://localhost:3000")
|
||||
So(loadedConfig.Database.Database, ShouldEqual, "default_db")
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
// 清理测试目录
|
||||
os.RemoveAll(testDir)
|
||||
})
|
||||
})
|
||||
|
||||
Reset(func() {
|
||||
// 恢复原始环境变量
|
||||
if originalEnvVars != nil {
|
||||
for key, value := range originalEnvVars {
|
||||
if value == "" {
|
||||
os.Unsetenv(key)
|
||||
} else {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestConfigValidation 测试配置验证
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
Convey("配置验证测试", t, func() {
|
||||
Convey("当配置为空时", func() {
|
||||
config := &config.Config{}
|
||||
|
||||
Convey("应该检测到缺失的必需配置", func() {
|
||||
So(config.App.Mode, ShouldBeEmpty)
|
||||
So(config.Http.Port, ShouldEqual, 0)
|
||||
So(config.Database.Host, ShouldBeEmpty)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当配置端口无效时", func() {
|
||||
config := &config.Config{
|
||||
Http: config.HttpConfig{
|
||||
Port: -1,
|
||||
},
|
||||
}
|
||||
|
||||
Convey("应该检测到无效端口", func() {
|
||||
So(config.Http.Port, ShouldBeLessThan, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("当配置模式有效时", func() {
|
||||
validModes := []string{"development", "production", "testing"}
|
||||
|
||||
for _, mode := range validModes {
|
||||
config := &config.Config{
|
||||
App: config.AppConfig{
|
||||
Mode: mode,
|
||||
},
|
||||
}
|
||||
|
||||
Convey("模式 "+mode+" 应该是有效的", func() {
|
||||
So(config.App.Mode, ShouldBeIn, validModes)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestConfigDefaults 测试配置默认值
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
Convey("配置默认值测试", t, func() {
|
||||
Convey("当创建新配置时", func() {
|
||||
config := &config.Config{}
|
||||
|
||||
Convey("应该有合理的默认值", func() {
|
||||
// 测试应用的默认值
|
||||
So(config.App.Mode, ShouldEqual, "development")
|
||||
|
||||
// 测试HTTP的默认值
|
||||
So(config.Http.Port, ShouldEqual, 8080)
|
||||
|
||||
// 测试数据库的默认值
|
||||
So(config.Database.Port, ShouldEqual, 5432)
|
||||
So(config.Database.SslMode, ShouldEqual, "disable")
|
||||
|
||||
// 测试日志的默认值
|
||||
So(config.Log.Level, ShouldEqual, "info")
|
||||
So(config.Log.Format, ShouldEqual, "json")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// TestConfigHelpers 测试配置辅助函数
|
||||
func TestConfigHelpers(t *testing.T) {
|
||||
Convey("配置辅助函数测试", t, func() {
|
||||
Convey("当使用配置辅助函数时", func() {
|
||||
config := &config.Config{
|
||||
App: config.AppConfig{
|
||||
Mode: "production",
|
||||
BaseURI: "https://api.example.com",
|
||||
},
|
||||
Http: config.HttpConfig{
|
||||
Port: 443,
|
||||
},
|
||||
}
|
||||
|
||||
Convey("应该能够获取应用环境", func() {
|
||||
env := config.App.Mode
|
||||
So(env, ShouldEqual, "production")
|
||||
})
|
||||
|
||||
Convey("应该能够构建完整URL", func() {
|
||||
fullURL := config.App.BaseURI + "/api/v1/users"
|
||||
So(fullURL, ShouldEqual, "https://api.example.com/api/v1/users")
|
||||
})
|
||||
|
||||
Convey("应该能够判断HTTPS", func() {
|
||||
isHTTPS := config.Http.Port == 443
|
||||
So(isHTTPS, ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user