🔗 集成测试策略
集成测试确保 lzt 项目中不同组件之间的协作正常工作。本文档提供完整的集成测试策略和实践指南。
🎯 集成测试概览
测试层级
集成测试类型
- 组件集成 - 测试内部组件之间的交互
- 数据库集成 - 测试数据持久层的操作
- API 集成 - 测试 HTTP/gRPC 接口
- 外部服务集成 - 测试第三方服务集成
- 端到端集成 - 测试完整的用户场景
🗄️ 数据库集成测试
测试数据库设置
go
// internal/app/ledger/repository_test.go
//go:build integration
package ledger
import (
"context"
"testing"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestDB(t *testing.T) *gorm.DB {
// 使用内存 SQLite 数据库
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// 运行迁移
err = db.AutoMigrate(&Ledger{}, &Transaction{}, &Tag{}, &Budget{})
require.NoError(t, err)
// 清理函数
t.Cleanup(func() {
sqlDB, err := db.DB()
if err == nil {
sqlDB.Close()
}
})
return db
}
func TestTransactionRepository_Integration(t *testing.T) {
db := setupTestDB(t)
repo := NewGormTransactionRepository(db)
t.Run("Create and Get Transaction", func(t *testing.T) {
// 创建测试数据
tx := &Transaction{
ID: "test-tx-1",
LedgerID: "test-ledger-1",
Type: TransactionTypeExpense,
Amount: Money{Amount: 1000, Currency: "CNY"},
Description: "Test transaction",
}
// 创建交易
err := repo.Create(context.Background(), tx)
require.NoError(t, err)
// 检索交易
retrieved, err := repo.GetByID(context.Background(), tx.ID)
require.NoError(t, err)
// 验证数据一致性
assert.Equal(t, tx.ID, retrieved.ID)
assert.Equal(t, tx.LedgerID, retrieved.LedgerID)
assert.Equal(t, tx.Type, retrieved.Type)
assert.Equal(t, tx.Amount.Amount, retrieved.Amount.Amount)
assert.Equal(t, tx.Description, retrieved.Description)
assert.NotZero(t, retrieved.CreatedAt)
})
t.Run("List Transactions with Filter", func(t *testing.T) {
// 创建多个测试交易
transactions := []*Transaction{
{
ID: "tx-1",
LedgerID: "ledger-1",
Type: TransactionTypeExpense,
Amount: Money{Amount: 1000, Currency: "CNY"},
},
{
ID: "tx-2",
LedgerID: "ledger-1",
Type: TransactionTypeIncome,
Amount: Money{Amount: 2000, Currency: "CNY"},
},
{
ID: "tx-3",
LedgerID: "ledger-2",
Type: TransactionTypeExpense,
Amount: Money{Amount: 1500, Currency: "CNY"},
},
}
for _, tx := range transactions {
err := repo.Create(context.Background(), tx)
require.NoError(t, err)
}
// 测试按账本过滤
filter := ListTransactionsFilter{
LedgerID: "ledger-1",
Limit: 10,
Offset: 0,
}
results, err := repo.List(context.Background(), filter)
require.NoError(t, err)
assert.Len(t, results, 2)
// 验证所有结果都属于同一账本
for _, tx := range results {
assert.Equal(t, "ledger-1", tx.LedgerID)
}
})
}数据库事务测试
go
func TestTransactionRepository_Concurrency(t *testing.T) {
db := setupTestDB(t)
repo := NewGormTransactionRepository(db)
t.Run("Concurrent Writes", func(t *testing.T) {
const numWorkers = 10
const transactionsPerWorker = 20
var wg sync.WaitGroup
errors := make(chan error, numWorkers*transactionsPerWorker)
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for j := 0; j < transactionsPerWorker; j++ {
tx := &Transaction{
ID: fmt.Sprintf("tx-%d-%d", workerID, j),
LedgerID: fmt.Sprintf("ledger-%d", workerID),
Type: TransactionTypeExpense,
Amount: Money{Amount: int64(j + 1), Currency: "CNY"},
}
if err := repo.Create(context.Background(), tx); err != nil {
errors <- err
}
}
}(i)
}
wg.Wait()
close(errors)
// 检查是否有错误
for err := range errors {
t.Errorf("Concurrent write failed: %v", err)
}
// 验证所有交易都已创建
totalCount := 0
for i := 0; i < numWorkers; i++ {
filter := ListTransactionsFilter{
LedgerID: fmt.Sprintf("ledger-%d", i),
Limit: 100,
}
results, err := repo.List(context.Background(), filter)
require.NoError(t, err)
totalCount += len(results)
}
assert.Equal(t, numWorkers*transactionsPerWorker, totalCount)
})
}🌐 API 集成测试
HTTP API 测试
go
// cmd/ledger/server_test.go
//go:build integration
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestServer(t *testing.T) *httptest.Server {
// 设置测试数据库
db := setupTestDB(t)
// 创建服务实例
repo := NewGormTransactionRepository(db)
service := NewTransactionService(repo)
handler := NewHTTPHandler(service)
return httptest.NewServer(handler)
}
func TestHTTPAPI_Integration(t *testing.T) {
server := setupTestServer(t)
defer server.Close()
t.Run("Create Transaction via HTTP", func(t *testing.T) {
// 准备请求数据
reqData := map[string]interface{}{
"ledger_id": "test-ledger",
"type": "expense",
"amount": 1000,
"currency": "CNY",
"description": "Test transaction",
}
reqBody, err := json.Marshal(reqData)
require.NoError(t, err)
// 发送 HTTP 请求
resp, err := http.Post(
server.URL+"/api/v1/transactions",
"application/json",
bytes.NewBuffer(reqBody),
)
require.NoError(t, err)
defer resp.Body.Close()
// 验证响应
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var responseData map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&responseData)
require.NoError(t, err)
assert.NotEmpty(t, responseData["id"])
assert.Equal(t, reqData["ledger_id"], responseData["ledger_id"])
assert.Equal(t, reqData["type"], responseData["type"])
assert.Equal(t, float64(reqData["amount"].(int)), responseData["amount"])
})
t.Run("Get Transaction via HTTP", func(t *testing.T) {
// 首先创建一个交易
createReq := map[string]interface{}{
"ledger_id": "test-ledger",
"type": "income",
"amount": 2000,
"currency": "CNY",
"description": "Test income",
}
createBody, _ := json.Marshal(createReq)
createResp, err := http.Post(
server.URL+"/api/v1/transactions",
"application/json",
bytes.NewBuffer(createBody),
)
require.NoError(t, err)
defer createResp.Body.Close()
var createData map[string]interface{}
err = json.NewDecoder(createResp.Body).Decode(&createData)
require.NoError(t, err)
transactionID := createData["id"].(string)
// 获取交易
getResp, err := http.Get(server.URL + "/api/v1/transactions/" + transactionID)
require.NoError(t, err)
defer getResp.Body.Close()
assert.Equal(t, http.StatusOK, getResp.StatusCode)
var getData map[string]interface{}
err = json.NewDecoder(getResp.Body).Decode(&getData)
require.NoError(t, err)
assert.Equal(t, transactionID, getData["id"])
assert.Equal(t, createReq["type"], getData["type"])
assert.Equal(t, float64(createReq["amount"].(int)), getData["amount"])
})
}gRPC 集成测试
go
// internal/app/ledger/grpc_test.go
//go:build integration
package ledger
import (
"context"
"net"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
pb "lzt/gen/ledger/v1"
)
const bufSize = 1024 * 1024
func setupGRPCTestServer(t *testing.T) (*grpc.ClientConn, pb.LedgerServiceClient) {
// 创建内存监听器
lis := bufconn.Listen(bufSize)
// 设置测试数据库和服务
db := setupTestDB(t)
repo := NewGormTransactionRepository(db)
service := NewTransactionService(repo)
// 创建 gRPC 服务器
server := grpc.NewServer()
pb.RegisterLedgerServiceServer(server, NewGRPCServer(service))
// 启动服务器
go func() {
if err := server.Serve(lis); err != nil {
t.Logf("Server exited with error: %v", err)
}
}()
// 创建客户端连接
conn, err := grpc.DialContext(
context.Background(),
"bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithInsecure(),
)
require.NoError(t, err)
client := pb.NewLedgerServiceClient(conn)
t.Cleanup(func() {
conn.Close()
server.Stop()
lis.Close()
})
return conn, client
}
func TestGRPCAPI_Integration(t *testing.T) {
_, client := setupGRPCTestServer(t)
t.Run("Create Transaction via gRPC", func(t *testing.T) {
req := &pb.CreateTransactionRequest{
LedgerId: "test-ledger",
Type: pb.TransactionType_TRANSACTION_TYPE_EXPENSE,
Amount: &pb.Money{Amount: 1500, Currency: pb.Currency_CURRENCY_CNY},
Description: "gRPC test transaction",
}
resp, err := client.CreateTransaction(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Transaction)
assert.NotEmpty(t, resp.Transaction.Id)
assert.Equal(t, req.LedgerId, resp.Transaction.LedgerId)
assert.Equal(t, req.Type, resp.Transaction.Type)
assert.Equal(t, req.Amount.Amount, resp.Transaction.Amount.Amount)
assert.Equal(t, req.Description, resp.Transaction.Description)
})
t.Run("List Transactions via gRPC", func(t *testing.T) {
// 创建几个测试交易
for i := 0; i < 3; i++ {
req := &pb.CreateTransactionRequest{
LedgerId: "list-test-ledger",
Type: pb.TransactionType_TRANSACTION_TYPE_EXPENSE,
Amount: &pb.Money{Amount: int64((i + 1) * 1000), Currency: pb.Currency_CURRENCY_CNY},
}
_, err := client.CreateTransaction(context.Background(), req)
require.NoError(t, err)
}
// 列出交易
listReq := &pb.ListTransactionsRequest{
LedgerId: "list-test-ledger",
Pagination: &pb.PaginationRequest{
PageSize: 10,
},
}
listResp, err := client.ListTransactions(context.Background(), listReq)
require.NoError(t, err)
require.NotNil(t, listResp)
assert.Len(t, listResp.Transactions, 3)
assert.Equal(t, int64(3), listResp.Pagination.TotalCount)
// 验证排序(应该按创建时间降序)
for i := 0; i < len(listResp.Transactions)-1; i++ {
current := listResp.Transactions[i]
next := listResp.Transactions[i+1]
assert.True(t,
current.CreatedAt.AsTime().After(next.CreatedAt.AsTime()) ||
current.CreatedAt.AsTime().Equal(next.CreatedAt.AsTime()),
"Transactions should be sorted by creation time (desc)",
)
}
})
}🔄 服务集成测试
跨服务集成
go
func TestLedgerBudgetIntegration(t *testing.T) {
db := setupTestDB(t)
// 创建服务实例
ledgerRepo := NewGormLedgerRepository(db)
transactionRepo := NewGormTransactionRepository(db)
budgetRepo := NewGormBudgetRepository(db)
ledgerService := NewLedgerService(ledgerRepo)
transactionService := NewTransactionService(transactionRepo)
budgetService := NewBudgetService(budgetRepo, transactionService)
t.Run("Budget Usage Calculation Integration", func(t *testing.T) {
// 1. 创建账本
ledger, err := ledgerService.CreateLedger(context.Background(), &CreateLedgerRequest{
Name: "Integration Test Ledger",
Type: LedgerTypePersonal,
})
require.NoError(t, err)
// 2. 创建预算
budget, err := budgetService.CreateBudget(context.Background(), &CreateBudgetRequest{
LedgerID: ledger.ID,
Name: "Monthly Budget",
Amount: Money{Amount: 10000, Currency: "CNY"},
Period: BudgetPeriodMonthly,
})
require.NoError(t, err)
// 3. 创建一些交易
transactions := []struct {
amount int64
txType TransactionType
}{
{3000, TransactionTypeExpense},
{2000, TransactionTypeExpense},
{5000, TransactionTypeIncome}, // 收入不影响预算
{1000, TransactionTypeExpense},
}
for i, tx := range transactions {
_, err := transactionService.CreateTransaction(context.Background(), &CreateTransactionRequest{
LedgerID: ledger.ID,
Type: tx.txType,
Amount: Money{Amount: tx.amount, Currency: "CNY"},
Description: fmt.Sprintf("Test transaction %d", i+1),
})
require.NoError(t, err)
}
// 4. 计算预算使用情况
usage, err := budgetService.GetBudgetUsage(context.Background(), budget.ID)
require.NoError(t, err)
// 5. 验证计算结果
expectedUsed := int64(3000 + 2000 + 1000) // 只有支出计入预算
expectedPercentage := float64(expectedUsed) / float64(budget.Amount.Amount) * 100
assert.Equal(t, expectedUsed, usage.UsedAmount.Amount)
assert.Equal(t, expectedPercentage, usage.Percentage)
assert.Equal(t, budget.Amount.Amount-expectedUsed, usage.RemainingAmount.Amount)
})
}事件驱动集成测试
go
func TestEventDrivenIntegration(t *testing.T) {
db := setupTestDB(t)
eventBus := NewInMemoryEventBus()
// 设置服务
transactionService := NewTransactionService(db, eventBus)
notificationService := NewNotificationService(eventBus)
analyticsService := NewAnalyticsService(db, eventBus)
// 启动事件监听器
go notificationService.Start()
go analyticsService.Start()
t.Run("Transaction Created Event Flow", func(t *testing.T) {
// 创建一个高额交易,应该触发通知
req := &CreateTransactionRequest{
LedgerID: "test-ledger",
Type: TransactionTypeExpense,
Amount: Money{Amount: 50000, Currency: "CNY"}, // 高额交易
Description: "大额支出测试",
}
tx, err := transactionService.CreateTransaction(context.Background(), req)
require.NoError(t, err)
// 等待事件处理
time.Sleep(100 * time.Millisecond)
// 验证通知服务收到了事件
notifications := notificationService.GetSentNotifications()
assert.Len(t, notifications, 1)
assert.Contains(t, notifications[0].Message, "大额支出")
assert.Equal(t, tx.ID, notifications[0].TransactionID)
// 验证分析服务更新了统计
stats := analyticsService.GetLedgerStats("test-ledger")
assert.Equal(t, int64(1), stats.TransactionCount)
assert.Equal(t, req.Amount.Amount, stats.TotalExpense.Amount)
})
}🧪 测试环境管理
Docker Compose 测试环境
yaml
# docker-compose.test.yml
version: '3.8'
services:
mysql-test:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: testpassword
MYSQL_DATABASE: ledger_test
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
ports:
- "3307:3306"
volumes:
- ./scripts/test-schema.sql:/docker-entrypoint-initdb.d/schema.sql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 5s
retries: 10
redis-test:
image: redis:7-alpine
ports:
- "6380:6379"
command: redis-server --appendonly yes
ledger-test:
build: .
depends_on:
mysql-test:
condition: service_healthy
redis-test:
condition: service_started
environment:
- DB_HOST=mysql-test
- DB_PORT=3306
- DB_USER=test_user
- DB_PASSWORD=test_password
- DB_NAME=ledger_test
- REDIS_HOST=redis-test
- REDIS_PORT=6379
command: ["go", "test", "-tags=integration", "./..."]测试配置管理
go
// internal/pkg/config/test_config.go
func NewTestConfig() *Config {
return &Config{
Database: DatabaseConfig{
Driver: "mysql",
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 3307),
User: getEnv("DB_USER", "test_user"),
Password: getEnv("DB_PASSWORD", "test_password"),
Name: getEnv("DB_NAME", "ledger_test"),
},
Redis: RedisConfig{
Host: getEnv("REDIS_HOST", "localhost"),
Port: getEnvAsInt("REDIS_PORT", 6380),
},
Server: ServerConfig{
Port: getEnvAsInt("SERVER_PORT", 8081),
},
}
}📊 集成测试最佳实践
1. 测试隔离
go
func TestWithTransactionRollback(t *testing.T) {
db := setupTestDB(t)
// 开始事务
tx := db.Begin()
defer func() {
// 测试结束后回滚事务
tx.Rollback()
}()
repo := NewGormTransactionRepository(tx)
// 执行测试...
// 所有数据库操作都在事务中,测试结束后会回滚
}2. 并行测试
go
func TestConcurrentOperations(t *testing.T) {
// 标记为并行测试
t.Parallel()
db := setupTestDB(t)
// 使用独立的数据库实例或命名空间
testNamespace := fmt.Sprintf("test_%d", time.Now().UnixNano())
// 执行测试...
}3. 测试数据管理
go
type TestDataManager struct {
db *gorm.DB
}
func NewTestDataManager(db *gorm.DB) *TestDataManager {
return &TestDataManager{db: db}
}
func (m *TestDataManager) CreateTestLedger(name string) *Ledger {
ledger := &Ledger{
ID: generateTestID(),
Name: name,
Type: LedgerTypePersonal,
}
err := m.db.Create(ledger).Error
if err != nil {
panic(fmt.Sprintf("Failed to create test ledger: %v", err))
}
return ledger
}
func (m *TestDataManager) CreateTestTransaction(ledgerID string, amount int64) *Transaction {
tx := &Transaction{
ID: generateTestID(),
LedgerID: ledgerID,
Type: TransactionTypeExpense,
Amount: Money{Amount: amount, Currency: "CNY"},
}
err := m.db.Create(tx).Error
if err != nil {
panic(fmt.Sprintf("Failed to create test transaction: %v", err))
}
return tx
}
func (m *TestDataManager) Cleanup() {
// 清理测试数据
m.db.Exec("DELETE FROM transactions")
m.db.Exec("DELETE FROM ledgers")
}🚀 CI/CD 集成测试
GitHub Actions 配置
yaml
name: Integration Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
integration-tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: ledger_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.24.3'
- name: Wait for MySQL
run: |
while ! mysqladmin ping -h127.0.0.1 -P3306 -uroot -ppassword --silent; do
sleep 1
done
- name: Run integration tests
run: go test -tags=integration -v ./...
env:
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_USER: root
DB_PASSWORD: password
DB_NAME: ledger_test📚 相关资源
项目测试文档
- TDD 测试驱动开发 - TDD 实践方法
- 单元测试指南 - 单元测试最佳实践
- 基准测试 - 性能测试指南
外部参考
💡 集成测试建议: 集成测试应该关注组件间的交互和数据流。保持测试环境的一致性,使用适当的测试替身来隔离外部依赖。