package logs_test import ( "bytes" "context" "encoding/json" "errors" "strings" "testing" "time" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "github.com/stretchr/testify/assert" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // captureOutput 是一个辅助函数,用于捕获 logger 的输出到内存缓冲区 func captureOutput(cfg config.LogConfig) (*logs.Logger, *bytes.Buffer) { var buf bytes.Buffer encoder := logs.GetEncoder(cfg.Format) writer := zapcore.AddSync(&buf) level := zap.NewAtomicLevel() _ = level.UnmarshalText([]byte(cfg.Level)) core := zapcore.NewCore(encoder, writer, level) // 匹配 logs.go 中 NewLogger 的行为,添加调用者信息 zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) logger := &logs.Logger{SugaredLogger: zapLogger.Sugar()} return logger, &buf } func TestNewLogger(t *testing.T) { t.Run("日志级别应生效", func(t *testing.T) { // 1. 创建一个级别为 WARN 的 logger logger, buf := captureOutput(config.LogConfig{Level: "warn", Format: "console"}) // 2. 调用不同级别的日志方法 logger.Info("这条 info 日志不应被打印") logger.Warn("这条 warn 日志应该被打印") // 3. 断言输出 output := buf.String() assert.NotContains(t, output, "这条 info 日志不应被打印") assert.Contains(t, output, "这条 warn 日志应该被打印") }) t.Run("JSON 格式应生效", func(t *testing.T) { // 1. 创建一个格式为 JSON 的 logger logger, buf := captureOutput(config.LogConfig{Level: "info", Format: "json"}) // 2. 打印一条日志 logger.Info("测试json输出") // 3. 断言输出 output := buf.String() // 验证它是否是合法的 JSON,并且包含预期的键值对 var logEntry map[string]interface{} // 注意:由于日志库可能会在行尾添加换行符,我们先 trim space err := json.Unmarshal([]byte(strings.TrimSpace(output)), &logEntry) assert.NoError(t, err, "日志输出应为合法的JSON") assert.Equal(t, "INFO", logEntry["level"]) assert.Equal(t, "测试json输出", logEntry["msg"]) }) t.Run("文件日志构造函数不应 panic", func(t *testing.T) { // 这个测试保持原样,只验证构造函数在启用文件时不会崩溃 // 注意:我们不在单元测试中实际写入文件 cfgFile := config.LogConfig{ Level: "info", EnableFile: true, FilePath: "test.log", // 在测试环境中,这个文件不会被真正创建 } assert.NotPanics(t, func() { logs.NewLogger(cfgFile) }) }) } func TestLogger_Write_ForGin(t *testing.T) { logger, buf := captureOutput(config.LogConfig{Level: "info"}) ginLog := "[GIN-debug] Listening and serving HTTP on :8080\n" _, err := logger.Write([]byte(ginLog)) assert.NoError(t, err) output := buf.String() // logger.Write 会将 gin 的日志转为 info 级别 assert.Contains(t, output, "INFO") assert.Contains(t, output, strings.TrimSpace(ginLog)) } func TestGormLogger(t *testing.T) { logger, buf := captureOutput(config.LogConfig{Level: "debug"}) // 设置为 debug 以捕获所有级别 gormLogger := logs.NewGormLogger(logger) // 模拟 GORM 的 Trace 调用参数 ctx := context.Background() sql := "SELECT * FROM users WHERE id = 1" rows := int64(1) fc := func() (string, int64) { return sql, rows } t.Run("慢查询应记录为警告", func(t *testing.T) { buf.Reset() // 模拟一个耗时超过 200ms 的查询 begin := time.Now().Add(-300 * time.Millisecond) gormLogger.Trace(ctx, begin, fc, nil) output := buf.String() assert.Contains(t, output, "WARN", "应包含 WARN 级别") assert.Contains(t, output, "[GORM] slow query", "应包含慢查询信息") assert.Contains(t, output, "SELECT * FROM users WHERE id = 1", "应包含 SQL 语句") }) t.Run("普通错误应记录为Error", func(t *testing.T) { buf.Reset() queryError := errors.New("syntax error") gormLogger.Trace(ctx, time.Now(), fc, queryError) output := buf.String() assert.Contains(t, output, "ERROR") assert.Contains(t, output, "[GORM] error: syntax error") }) t.Run("当SkipErrRecordNotFound为true时应跳过RecordNotFound错误", func(t *testing.T) { buf.Reset() // 确保默认设置是 true gormLogger.SkipErrRecordNotFound = true // 错误必须包含 "record not found" 字符串以匹配 logs.go 中的判断逻辑 queryError := errors.New("record not found") gormLogger.Trace(ctx, time.Now(), fc, queryError) assert.Empty(t, buf.String(), "开启 SkipErrRecordNotFound 后,record not found 错误不应产生任何日志") }) t.Run("当SkipErrRecordNotFound为false时应记录RecordNotFound错误", func(t *testing.T) { buf.Reset() // 手动将 SkipErrRecordNotFound 设置为 false gormLogger.SkipErrRecordNotFound = false queryError := errors.New("record not found") gormLogger.Trace(ctx, time.Now(), fc, queryError) // 恢复设置,避免影响其他测试 gormLogger.SkipErrRecordNotFound = true output := buf.String() assert.NotEmpty(t, output, "关闭 SkipErrRecordNotFound 后,record not found 错误应该产生日志") assert.Contains(t, output, "ERROR") assert.Contains(t, output, "[GORM] error: record not found") }) t.Run("正常查询应记录为Debug", func(t *testing.T) { buf.Reset() // 模拟一个快速查询 gormLogger.Trace(ctx, time.Now(), fc, nil) output := buf.String() assert.Contains(t, output, "DEBUG") // 正常查询是 Debug 级别 assert.Contains(t, output, "[GORM] trace") }) }