Testing Guide - zhoudm1743/go-util GitHub Wiki
本指南将帮助您全面了解 Go-Util 项目的测试策略、测试编写规范和最佳实践。
🎯 测试策略概览
测试金字塔
/\
/ \ E2E 测试 (5%)
/____\ 集成测试 (15%)
/______\ 单元测试 (80%)
- 单元测试: 测试单个函数/方法的功能
- 集成测试: 测试模块间的交互
- 端到端测试: 测试完整的用户场景
测试原则
- ✅ 快速: 单元测试应该在毫秒级完成
- ✅ 独立: 测试之间不应相互依赖
- ✅ 可重复: 多次运行结果一致
- ✅ 自验证: 测试结果明确(通过/失败)
- ✅ 及时: 测试应该在代码变更时立即运行
🧪 单元测试规范
1. 测试文件组织
// 文件结构
types_str.go // 源码文件
types_str_test.go // 测试文件
2. 测试函数命名
// 标准命名格式: TestFunctionName_Scenario
func TestXStr_Upper_BasicConversion(t *testing.T) {
// 测试基础大写转换
}
func TestXStr_Upper_EmptyString(t *testing.T) {
// 测试空字符串场景
}
func TestXStr_Upper_UnicodeCharacters(t *testing.T) {
// 测试Unicode字符场景
}
3. 表驱动测试模式
func TestXStr_IsEmail(t *testing.T) {
tests := []struct {
name string
input string
expected bool
reason string
}{
{
name: "有效邮箱",
input: "[email protected]",
expected: true,
reason: "标准邮箱格式",
},
{
name: "无效邮箱_缺少@",
input: "userexample.com",
expected: false,
reason: "缺少@符号",
},
{
name: "无效邮箱_缺少域名",
input: "user@",
expected: false,
reason: "缺少域名部分",
},
{
name: "空字符串",
input: "",
expected: false,
reason: "空字符串不是有效邮箱",
},
{
name: "复杂邮箱",
input: "[email protected]",
expected: true,
reason: "复杂但有效的邮箱格式",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := util.Str(tt.input).IsEmail()
if result != tt.expected {
t.Errorf("IsEmail(%q) = %v, want %v (%s)",
tt.input, result, tt.expected, tt.reason)
}
})
}
}
4. 测试辅助函数
// 测试辅助函数
func assertStringEqual(t *testing.T, actual, expected string) {
t.Helper()
if actual != expected {
t.Errorf("got %q, want %q", actual, expected)
}
}
func assertStringContains(t *testing.T, str, substr string) {
t.Helper()
if !strings.Contains(str, substr) {
t.Errorf("string %q does not contain %q", str, substr)
}
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func assertError(t *testing.T, err error, expectedMsg string) {
t.Helper()
if err == nil {
t.Error("expected error but got none")
return
}
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("error %q does not contain %q", err.Error(), expectedMsg)
}
}
// 使用示例
func TestXStr_ToInt_ValidNumber(t *testing.T) {
result, err := util.Str("123").ToInt()
assertNoError(t, err)
if result != 123 {
t.Errorf("ToInt() = %d, want 123", result)
}
}
5. 边界条件测试
func TestXStr_Slice_BoundaryConditions(t *testing.T) {
tests := []struct {
name string
input string
start int
end int
expected string
hasError bool
}{
// 正常情况
{
name: "正常切片",
input: "hello world",
start: 0,
end: 5,
expected: "hello",
hasError: false,
},
// 边界情况
{
name: "起始位置为0",
input: "hello",
start: 0,
end: 0,
expected: "",
hasError: false,
},
{
name: "结束位置等于长度",
input: "hello",
start: 0,
end: 5,
expected: "hello",
hasError: false,
},
{
name: "空字符串",
input: "",
start: 0,
end: 0,
expected: "",
hasError: false,
},
// 错误情况
{
name: "起始位置超出范围",
input: "hello",
start: 10,
end: 15,
expected: "",
hasError: true,
},
{
name: "结束位置超出范围",
input: "hello",
start: 0,
end: 10,
expected: "",
hasError: true,
},
{
name: "起始位置大于结束位置",
input: "hello",
start: 3,
end: 1,
expected: "",
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := util.Str(tt.input).Slice(tt.start, tt.end)
if tt.hasError {
if err == nil {
t.Errorf("expected error but got none")
}
return
}
assertNoError(t, err)
assertStringEqual(t, result, tt.expected)
})
}
}
🔗 集成测试
1. 模块间交互测试
func TestStringArrayIntegration(t *testing.T) {
// 测试字符串和数组工具的集成使用
userEmails := []string{
" [email protected] ",
"[email protected]",
"invalid-email",
" [email protected] ",
}
// 清理和验证邮箱
validEmails := util.ArraysFromSlice(userEmails).
Map(func(email string) string {
return util.Str(email).Trim().Lower().String()
}).
Filter(func(email string) bool {
return util.Str(email).IsEmail()
}).
ToSlice()
expected := []string{
"[email protected]",
"[email protected]",
"[email protected]",
}
if len(validEmails) != len(expected) {
t.Errorf("expected %d valid emails, got %d", len(expected), len(validEmails))
}
for i, email := range validEmails {
if email != expected[i] {
t.Errorf("email[%d] = %q, want %q", i, email, expected[i])
}
}
}
2. JSON 操作集成测试
func TestJSONOperationIntegration(t *testing.T) {
// 测试复杂的JSON操作场景
jsonData := `{
"users": [
{"name": "Alice", "email": "[email protected]", "age": 25},
{"name": "Bob", "email": "[email protected]", "age": 30}
],
"metadata": {
"total": 2,
"page": 1
}
}`
// 使用 JSONx 进行操作
j, err := util.ParseJSON(jsonData)
assertNoError(t, err)
// 提取用户邮箱
emails := j.Get("users").Array().Map(func(user *util.JSON) string {
return user.Get("email").String()
})
// 验证邮箱格式
validEmails := util.ArraysFromSlice(emails).
Filter(func(email string) bool {
return util.Str(email).IsEmail()
}).
ToSlice()
if len(validEmails) != 2 {
t.Errorf("expected 2 valid emails, got %d", len(validEmails))
}
// 更新用户年龄
j.Get("users").Array().ForEach(func(i int, user *util.JSON) bool {
currentAge := user.Get("age").Int()
user.Set("age", currentAge+1)
return true
})
// 验证更新
aliceAge := j.Get("users.0.age").Int()
if aliceAge != 26 {
t.Errorf("Alice's age should be 26, got %d", aliceAge)
}
}
3. 时间处理集成测试
func TestTimeProcessingIntegration(t *testing.T) {
// 测试时间处理的完整流程
timestamps := []string{
"2023-12-25T10:30:00Z",
"2023-12-25T15:45:00+08:00",
"invalid-time",
"2023-12-26T08:00:00Z",
}
// 解析和处理时间
validTimes := util.ArraysFromSlice(timestamps).
Map(func(ts string) *util.XTime {
time, err := util.ParseTime(ts)
if err != nil {
return nil
}
return time
}).
Filter(func(time *util.XTime) bool {
return time != nil
}).
Map(func(time *util.XTime) *util.XTime {
return time.ToUTC()
}).
SortWith(func(a, b *util.XTime) bool {
return a.Before(b)
}).
ToSlice()
if len(validTimes) != 3 {
t.Errorf("expected 3 valid times, got %d", len(validTimes))
}
// 验证排序结果
if !validTimes[0].Before(validTimes[1]) {
t.Error("times are not properly sorted")
}
// 计算时间范围
timeRange := util.NewTimeRange(validTimes[0], validTimes[len(validTimes)-1])
duration := timeRange.Duration()
if duration <= 0 {
t.Error("time range duration should be positive")
}
}
⚡ 性能测试
1. 基准测试
func BenchmarkXStr_Operations(b *testing.B) {
testString := " Hello World, This is a Test String! "
b.Run("Single_Operation", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = util.Str(testString).Upper().String()
}
})
b.Run("Chained_Operations", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := util.Str(testString).
Trim().
Lower().
ReplaceRegex(`\s+`, "_").
Snake2Camel().
String()
_ = result
}
})
b.Run("Memory_Allocation", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
str := util.Str(testString)
for j := 0; j < 10; j++ {
str = str.Upper().Lower()
}
_ = str.String()
}
})
}
func BenchmarkXArray_Operations(b *testing.B) {
data := make([]int, 10000)
for i := range data {
data[i] = i
}
b.Run("Filter", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = util.ArraysFromSlice(data).
Filter(func(n int) bool { return n%2 == 0 }).
ToSlice()
}
})
b.Run("Map", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = util.ArraysFromSlice(data).
Map(func(n int) int { return n * 2 }).
ToSlice()
}
})
b.Run("Chained", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = util.ArraysFromSlice(data).
Filter(func(n int) bool { return n%2 == 0 }).
Map(func(n int) int { return n * 2 }).
Take(100).
ToSlice()
}
})
}
2. 内存测试
func TestMemoryUsage(t *testing.T) {
// 测试内存使用情况
const iterations = 1000
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
// 执行操作
for i := 0; i < iterations; i++ {
data := make([]string, 100)
for j := range data {
data[j] = fmt.Sprintf("item_%d", j)
}
_ = util.ArraysFromSlice(data).
Map(func(s string) string {
return util.Str(s).Upper().String()
}).
Filter(func(s string) bool {
return util.Str(s).Contains("ITEM")
}).
ToSlice()
}
runtime.GC()
runtime.ReadMemStats(&m2)
allocsPerOp := (m2.Mallocs - m1.Mallocs) / iterations
bytesPerOp := (m2.TotalAlloc - m1.TotalAlloc) / iterations
t.Logf("Memory per operation: %d allocs, %d bytes", allocsPerOp, bytesPerOp)
// 设置合理的内存使用阈值
if allocsPerOp > 1000 {
t.Errorf("Too many allocations per operation: %d", allocsPerOp)
}
if bytesPerOp > 100000 {
t.Errorf("Too much memory per operation: %d bytes", bytesPerOp)
}
}
3. 并发安全测试
func TestConcurrencySafety(t *testing.T) {
const goroutines = 100
const operations = 1000
// 测试 SafeMap 的并发安全性
safeMap := util.NewSafeMap[string, int]()
var wg sync.WaitGroup
wg.Add(goroutines)
// 启动多个goroutine进行并发操作
for i := 0; i < goroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < operations; j++ {
key := fmt.Sprintf("key_%d_%d", id, j)
value := id*operations + j
// 写操作
safeMap.Set(key, value)
// 读操作
if val, exists := safeMap.Get(key); exists {
if val != value {
t.Errorf("Expected %d, got %d for key %s", value, val, key)
}
}
// 删除操作
if j%10 == 0 {
safeMap.Delete(key)
}
}
}(i)
}
wg.Wait()
// 验证最终状态
finalSize := safeMap.Size()
t.Logf("Final map size: %d", finalSize)
// 应该有一些数据被保留(未被删除的)
if finalSize == 0 {
t.Error("SafeMap should not be empty after concurrent operations")
}
}
🧩 Mock 和 Stub
1. 接口 Mock
// 定义接口
type UserRepository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
// Mock 实现
type MockUserRepository struct {
users map[string]*User
err error
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[string]*User),
}
}
func (m *MockUserRepository) GetUser(id string) (*User, error) {
if m.err != nil {
return nil, m.err
}
user, exists := m.users[id]
if !exists {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepository) SaveUser(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
func (m *MockUserRepository) SetError(err error) {
m.err = err
}
func (m *MockUserRepository) AddUser(user *User) {
m.users[user.ID] = user
}
// 使用 Mock 进行测试
func TestUserService_ProcessUser(t *testing.T) {
mockRepo := NewMockUserRepository()
service := NewUserService(mockRepo)
// 测试成功场景
t.Run("Success", func(t *testing.T) {
user := &User{ID: "1", Name: "Alice", Email: "[email protected]"}
mockRepo.AddUser(user)
result, err := service.ProcessUser("1")
assertNoError(t, err)
assertStringEqual(t, result.Name, "Alice")
})
// 测试错误场景
t.Run("UserNotFound", func(t *testing.T) {
_, err := service.ProcessUser("nonexistent")
assertError(t, err, "user not found")
})
// 测试仓库错误
t.Run("RepositoryError", func(t *testing.T) {
mockRepo.SetError(errors.New("database error"))
_, err := service.ProcessUser("1")
assertError(t, err, "database error")
})
}
2. HTTP 客户端 Mock
// HTTP 客户端接口
type HTTPClient interface {
Get(url string) (*http.Response, error)
Post(url string, body io.Reader) (*http.Response, error)
}
// Mock HTTP 客户端
type MockHTTPClient struct {
responses map[string]*http.Response
errors map[string]error
}
func NewMockHTTPClient() *MockHTTPClient {
return &MockHTTPClient{
responses: make(map[string]*http.Response),
errors: make(map[string]error),
}
}
func (m *MockHTTPClient) Get(url string) (*http.Response, error) {
if err, exists := m.errors[url]; exists {
return nil, err
}
if resp, exists := m.responses[url]; exists {
return resp, nil
}
return nil, errors.New("no mock response configured")
}
func (m *MockHTTPClient) SetResponse(url string, statusCode int, body string) {
m.responses[url] = &http.Response{
StatusCode: statusCode,
Body: io.NopCloser(strings.NewReader(body)),
}
}
func (m *MockHTTPClient) SetError(url string, err error) {
m.errors[url] = err
}
// 使用示例
func TestAPIClient_GetUserProfile(t *testing.T) {
mockClient := NewMockHTTPClient()
apiClient := NewAPIClient(mockClient)
t.Run("Success", func(t *testing.T) {
responseBody := `{"id": "123", "name": "Alice"}`
mockClient.SetResponse("https://api.example.com/users/123", 200, responseBody)
profile, err := apiClient.GetUserProfile("123")
assertNoError(t, err)
assertStringEqual(t, profile.Name, "Alice")
})
t.Run("NetworkError", func(t *testing.T) {
mockClient.SetError("https://api.example.com/users/456", errors.New("network error"))
_, err := apiClient.GetUserProfile("456")
assertError(t, err, "network error")
})
}
🔄 测试生命周期
1. SetUp 和 TearDown
func TestMain(m *testing.M) {
// 全局设置
setup()
// 运行测试
code := m.Run()
// 全局清理
teardown()
os.Exit(code)
}
func setup() {
// 初始化测试环境
log.Println("Setting up test environment...")
// 创建临时目录
os.MkdirAll("testdata/temp", 0755)
// 初始化数据库连接
initTestDatabase()
}
func teardown() {
// 清理测试环境
log.Println("Cleaning up test environment...")
// 删除临时文件
os.RemoveAll("testdata/temp")
// 关闭数据库连接
closeTestDatabase()
}
// 测试套件结构
type TestSuite struct {
tempDir string
db *sql.DB
}
func (ts *TestSuite) SetUpTest(t *testing.T) {
// 每个测试前的设置
var err error
ts.tempDir, err = os.MkdirTemp("", "test_")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
ts.db, err = sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
}
func (ts *TestSuite) TearDownTest(t *testing.T) {
// 每个测试后的清理
if ts.tempDir != "" {
os.RemoveAll(ts.tempDir)
}
if ts.db != nil {
ts.db.Close()
}
}
2. 子测试和并行测试
func TestStringOperations(t *testing.T) {
t.Run("BasicOperations", func(t *testing.T) {
t.Parallel() // 并行运行
t.Run("Upper", func(t *testing.T) {
result := util.Str("hello").Upper().String()
assertStringEqual(t, result, "HELLO")
})
t.Run("Lower", func(t *testing.T) {
result := util.Str("HELLO").Lower().String()
assertStringEqual(t, result, "hello")
})
t.Run("Trim", func(t *testing.T) {
result := util.Str(" hello ").Trim().String()
assertStringEqual(t, result, "hello")
})
})
t.Run("ValidationOperations", func(t *testing.T) {
t.Parallel() // 并行运行
t.Run("IsEmail", func(t *testing.T) {
if !util.Str("[email protected]").IsEmail() {
t.Error("Valid email should return true")
}
})
t.Run("IsNumeric", func(t *testing.T) {
if !util.Str("12345").IsNumeric() {
t.Error("Numeric string should return true")
}
})
})
}
📊 测试覆盖率
1. 生成覆盖率报告
# 运行测试并生成覆盖率
go test -v -cover -coverprofile=coverage.out ./...
# 查看覆盖率概要
go tool cover -func=coverage.out
# 生成 HTML 覆盖率报告
go tool cover -html=coverage.out -o coverage.html
# 设置覆盖率阈值
go test -cover -coverprofile=coverage.out ./... && \
go tool cover -func=coverage.out | grep "total:" | \
awk '{if ($3+0 < 80) exit 1}'
2. 覆盖率配置
// coverage_test.go
//go:build coverage
// +build coverage
package util
import (
"testing"
"os"
)
func TestCoverage(t *testing.T) {
// 确保所有导出函数都有测试覆盖
requiredCoverage := map[string]bool{
"Str": false,
"Arrays": false,
"Maps": false,
"Now": false,
"ParseJSON": false,
}
// 这里可以通过反射检查所有导出函数是否被测试覆盖
// 具体实现依据项目需求
}
func TestMain(m *testing.M) {
// 设置覆盖率相关环境变量
if os.Getenv("COVERAGE") != "" {
// 启用覆盖率收集
setupCoverageCollection()
}
code := m.Run()
if os.Getenv("COVERAGE") != "" {
// 处理覆盖率数据
processCoverageData()
}
os.Exit(code)
}
🐛 测试调试
1. 调试技巧
func TestDebugExample(t *testing.T) {
// 使用 t.Log 输出调试信息
t.Logf("Testing with input: %q", "test input")
// 条件性跳过测试
if testing.Short() {
t.Skip("Skipping test in short mode")
}
// 标记测试失败但继续执行
result := performOperation()
if result != expected {
t.Errorf("Got %v, want %v", result, expected)
}
// 立即停止测试
if criticalError {
t.Fatalf("Critical error occurred: %v", err)
}
// 使用 testify 进行断言(可选)
// assert.Equal(t, expected, result, "Values should be equal")
// require.NoError(t, err, "Should not return error")
}
2. 测试数据管理
// testdata 目录结构
// testdata/
// ├── valid_emails.txt
// ├── invalid_emails.txt
// ├── sample.json
// └── large_dataset.csv
func loadTestData(t *testing.T, filename string) []byte {
t.Helper()
data, err := os.ReadFile(filepath.Join("testdata", filename))
if err != nil {
t.Fatalf("Failed to load test data %s: %v", filename, err)
}
return data
}
func TestWithTestData(t *testing.T) {
// 加载测试数据
validEmails := strings.Split(string(loadTestData(t, "valid_emails.txt")), "\n")
invalidEmails := strings.Split(string(loadTestData(t, "invalid_emails.txt")), "\n")
// 测试有效邮箱
for _, email := range validEmails {
if email = strings.TrimSpace(email); email != "" {
if !util.Str(email).IsEmail() {
t.Errorf("Email %q should be valid", email)
}
}
}
// 测试无效邮箱
for _, email := range invalidEmails {
if email = strings.TrimSpace(email); email != "" {
if util.Str(email).IsEmail() {
t.Errorf("Email %q should be invalid", email)
}
}
}
}
🚀 持续集成测试
1. GitHub Actions 配置
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [1.19, 1.20, 1.21]
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Run tests
run: |
go test -v -race -cover -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
flags: unittests
name: codecov-umbrella
- name: Run benchmarks
run: go test -bench=. -benchmem ./...
- name: Check code quality
run: |
go vet ./...
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...
2. 测试脚本
#!/bin/bash
# scripts/test-all.sh
set -e
echo "🧪 Running comprehensive test suite..."
# 基础测试
echo "📋 Running unit tests..."
go test -v -race ./...
# 覆盖率测试
echo "📊 Generating coverage report..."
go test -v -race -cover -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# 性能测试
echo "⚡ Running benchmarks..."
go test -bench=. -benchmem -cpuprofile=cpu.prof -memprofile=mem.prof ./...
# 代码质量检查
echo "🔍 Running code quality checks..."
go vet ./...
golangci-lint run
# 安全检查
echo "🔒 Running security scan..."
gosec ./...
# 集成测试
echo "🔗 Running integration tests..."
go test -tags=integration -v ./...
# 压力测试
echo "💪 Running stress tests..."
go test -tags=stress -v ./...
echo "✅ All tests completed successfully!"
💬 获取帮助
如果您在测试编写中遇到问题:
- 🔍 查看FAQ - 常见测试问题解答
- 🐛 报告问题 - Bug反馈
- 💡 功能建议 - 新功能讨论
- 📧 邮件支持 - [email protected]
🎯 良好的测试是高质量代码的保证,让我们一起构建可靠的 Go-Util!