本文档详细说明了 IM 系统的测试策略、测试覆盖范围以及如何运行测试。
我们的测试策略包含三个层次:
- 测试单个函数、方法或类的行为
- 使用 Mock 对象隔离依赖
- 快速执行,提供即时反馈
- 覆盖边界条件和错误处理
- 测试多个组件之间的交互
- 使用真实的数据库、Redis 等依赖
- 验证服务间的 gRPC 调用
- 测试端到端的业务流程
- 测试完整的用户场景
- 从客户端到后端的完整流程
- 使用 Docker Compose 启动完整环境
- ✅
pkg/auth/jwt_test.go- JWT Token 生成和验证 - ✅
pkg/types/enums_test.go- 枚举类型和权限验证
- ✅
internal/user/service_test.go- 用户注册、登录、信息管理 - ✅
internal/router/service_test.go- 路由注册、心跳、在线状态 - ✅
internal/message/service_test.go- 消息发送、拉取、会话管理 - ✅
internal/file/service_test.go- 文件上传、下载、删除
func TestJWTManager_Generate(t *testing.T) {
manager := NewJWTManager("test-secret", 1*time.Hour)
token, err := manager.Generate(123, "device-001")
assert.NoError(t, err)
assert.NotEmpty(t, token)
}func TestService_Register(t *testing.T) {
repo := newMockRepository()
jwtManager := auth.NewJWTManager("test-secret", 1*time.Hour)
service := NewService(repo, jwtManager)
userID, err := service.Register(ctx, "testuser", "password123",
"test@example.com", "Test User")
assert.NoError(t, err)
assert.NotEqual(t, int64(0), userID)
}mkdir -p test/integration// test/integration/user_test.go
func TestUserServiceIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
// 连接真实数据库
db := setupTestDatabase(t)
defer cleanupTestDatabase(t, db)
repo := user.NewRepository(db)
jwtManager := auth.NewJWTManager("test-secret", 1*time.Hour)
service := user.NewService(repo, jwtManager)
// 测试完整的注册流程
userID, err := service.Register(context.Background(),
"testuser", "password123", "test@example.com", "Test User")
require.NoError(t, err)
assert.Greater(t, userID, int64(0))
// 测试登录
_, token, _, _, err := service.Login(context.Background(),
"testuser", "password123", "device-001")
require.NoError(t, err)
assert.NotEmpty(t, token)
}// test/integration/grpc_test.go
func TestGRPCServices(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
// 启动测试服务器
server := startTestServer(t)
defer server.Stop()
// 创建客户端连接
conn := createTestClient(t, server.Address())
defer conn.Close()
// 测试 User Service
userClient := pb.NewUserServiceClient(conn)
resp, err := userClient.Register(context.Background(), &pb.RegisterRequest{
Username: "testuser",
Password: "password123",
Email: "test@example.com",
Nickname: "Test User",
})
require.NoError(t, err)
assert.Greater(t, resp.UserId, int64(0))
}make testgo test -short ./...go test ./pkg/auth/...
go test ./internal/user/...
go test ./internal/router/...go test -run TestJWTManager_Generate ./pkg/auth/go test -v ./...go test -parallel 4 ./...make test-coverage或手动执行:
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.htmlgo tool cover -func=coverage.out- pkg/: 目标 90%+
- internal/*/service.go: 目标 85%+
- internal/*/repository.go: 目标 80%+
- 整体项目: 目标 75%+
type MockRepository struct {
users map[string]*User
createUserFunc func(ctx context.Context, username, password, email, nickname string) (*User, error)
}
func (m *MockRepository) CreateUser(ctx context.Context, username, password, email, nickname string) (*User, error) {
if m.createUserFunc != nil {
return m.createUserFunc(ctx, username, password, email, nickname)
}
// 默认实现
user := &User{
ID: int64(len(m.users) + 1),
Username: username,
Email: email,
Nickname: nickname,
}
m.users[username] = user
return user, nil
}type MockRedisClient struct {
data map[string]map[string]string
}
func (m *MockRedisClient) HSet(ctx context.Context, key string, values ...interface{}) *redis.IntCmd {
if m.data[key] == nil {
m.data[key] = make(map[string]string)
}
// 实现 mock 逻辑
cmd := redis.NewIntCmd(ctx)
cmd.SetVal(1)
return cmd
}// 函数: Test{PackageName}_{FunctionName}
func TestUserService_Register(t *testing.T) {}
// 方法: Test{StructName}_{MethodName}
func TestJWTManager_Generate(t *testing.T) {}
// 子测试: 使用描述性名称
t.Run("successful registration", func(t *testing.T) {})
t.Run("duplicate username error", func(t *testing.T) {})func TestConversationType_IsValid(t *testing.T) {
tests := []struct {
name string
convType ConversationType
want bool
}{
{"direct conversation", ConversationTypeDirect, true},
{"group conversation", ConversationTypeGroup, true},
{"invalid type", ConversationType("invalid"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.convType.IsValid()
assert.Equal(t, tt.want, got)
})
}
}// 推荐
assert.Equal(t, expected, actual)
assert.NoError(t, err)
require.NotNil(t, obj) // require 失败时立即停止
// 不推荐
if actual != expected {
t.Errorf("Expected %v, got %v", expected, actual)
}func TestUserService(t *testing.T) {
// 每个测试使用独立的 mock
t.Run("test case 1", func(t *testing.T) {
repo := newMockRepository() // 新实例
service := NewService(repo, jwtManager)
// 测试...
})
t.Run("test case 2", func(t *testing.T) {
repo := newMockRepository() // 新实例
service := NewService(repo, jwtManager)
// 测试...
})
}func TestFileUpload(t *testing.T) {
tmpFile := createTempFile(t)
defer os.Remove(tmpFile) // 确保清理
db := setupTestDB(t)
defer db.Close()
// 测试...
}func TestConcurrentAccess(t *testing.T) {
service := NewService()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 并发操作
service.DoSomething()
}()
}
wg.Wait()
}go test -v ./pkg/auth/ 2>&1 | grep "PASS\|FAIL"dlv test ./pkg/auth -- -test.run TestJWTManager_Generatego test -timeout 30s ./...name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Run tests
run: |
go test -v -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out在提交代码前,确保:
- 所有单元测试通过:
go test ./... - 测试覆盖率达标:
make test-coverage - 代码通过 lint 检查:
make lint - 没有数据竞争:
go test -race ./... - 集成测试通过:
go test ./test/integration/... - 新功能有对应的测试
- 边界条件和错误处理有测试覆盖
维护者: @dollarkillerx 最后更新: 2025-10-05