新增日志模块测试用例

This commit is contained in:
zyj
2026-03-04 14:19:31 +08:00
parent 7b01036f7c
commit 9a2eabcf43

View File

@@ -1 +1,491 @@
package logging package logging
import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/rs/zerolog"
)
// ==================== New / Init 构造测试 ====================
func TestNew_DefaultOptions(t *testing.T) {
logger, err := New(Options{})
if err != nil {
t.Fatalf("New() 默认选项不应报错, got: %v", err)
}
if logger == nil {
t.Fatal("New() 返回的 Logger 不应为 nil")
}
// 默认级别应为 Info
if logger.level != zerolog.InfoLevel {
t.Errorf("默认级别应为 Info, got: %s", logger.level)
}
if logger.quiet {
t.Error("默认不应为静默模式")
}
if logger.verbose {
t.Error("默认不应为详细模式")
}
if logger.logFile != nil {
t.Error("默认不应有日志文件")
}
}
func TestNew_WithLevel(t *testing.T) {
tests := []struct {
level string
expected zerolog.Level
}{
{"trace", zerolog.TraceLevel},
{"debug", zerolog.DebugLevel},
{"info", zerolog.InfoLevel},
{"warn", zerolog.WarnLevel},
{"error", zerolog.ErrorLevel},
{"fatal", zerolog.FatalLevel},
}
for _, tt := range tests {
t.Run(tt.level, func(t *testing.T) {
logger, err := New(Options{Level: tt.level, Quiet: true})
if err != nil {
t.Fatalf("New(Level=%s) 不应报错, got: %v", tt.level, err)
}
if logger.level != tt.expected {
t.Errorf("级别应为 %s, got: %s", tt.expected, logger.level)
}
})
}
}
func TestNew_InvalidLevel(t *testing.T) {
_, err := New(Options{Level: "invalid_level"})
if err == nil {
t.Fatal("无效日志级别应返回错误")
}
if !strings.Contains(err.Error(), "无效的日志级别") {
t.Errorf("错误消息应包含 '无效的日志级别', got: %s", err.Error())
}
}
func TestNew_VerboseOverridesLevel(t *testing.T) {
logger, err := New(Options{Level: "error", Verbose: true, Quiet: true})
if err != nil {
t.Fatalf("不应报错, got: %v", err)
}
// Verbose 应覆盖 Level强制设为 Debug
if logger.level != zerolog.DebugLevel {
t.Errorf("Verbose=true 时级别应为 Debug, got: %s", logger.level)
}
if !logger.verbose {
t.Error("verbose 标志应为 true")
}
}
func TestNew_QuietMode(t *testing.T) {
logger, err := New(Options{Quiet: true})
if err != nil {
t.Fatalf("不应报错, got: %v", err)
}
if !logger.quiet {
t.Error("quiet 标志应为 true")
}
}
func TestNew_WithLogFile(t *testing.T) {
tmpDir := t.TempDir()
logPath := filepath.Join(tmpDir, "test.log")
logger, err := New(Options{LogFile: logPath, Quiet: true})
if err != nil {
t.Fatalf("不应报错, got: %v", err)
}
defer logger.Close()
if logger.logFile == nil {
t.Fatal("logFile 不应为 nil")
}
// 验证文件已创建
if _, err := os.Stat(logPath); os.IsNotExist(err) {
t.Error("日志文件应已被创建")
}
}
func TestNew_LogFileCreatesDir(t *testing.T) {
tmpDir := t.TempDir()
logPath := filepath.Join(tmpDir, "subdir", "nested", "test.log")
logger, err := New(Options{LogFile: logPath, Quiet: true})
if err != nil {
t.Fatalf("不应报错, got: %v", err)
}
defer logger.Close()
// 验证嵌套目录已创建
dirPath := filepath.Dir(logPath)
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
t.Error("日志目录应已被自动创建")
}
}
// ==================== Init / G 全局实例测试 ====================
func TestInit_SetsGlobal(t *testing.T) {
// 保存并恢复原始全局实例
original := global
defer func() { global = original }()
logger, err := Init(Options{Quiet: true})
if err != nil {
t.Fatalf("Init() 不应报错, got: %v", err)
}
if G() != logger {
t.Error("G() 应返回 Init() 设置的全局实例")
}
}
func TestG_DefaultSafe(t *testing.T) {
// 即使未初始化G() 也应返回安全的 nop logger
original := global
defer func() { global = original }()
global = &Logger{zl: zerolog.Nop()}
log := G()
if log == nil {
t.Fatal("G() 不应返回 nil")
}
// 调用方法不应 panic
log.Info("安全调用测试")
log.Debugf("格式化 %s", "测试")
}
// ==================== 日志输出测试 ====================
// newBufferLogger 创建一个写入 buffer 的 Logger用于验证输出
func newBufferLogger(level zerolog.Level) (*Logger, *bytes.Buffer) {
buf := &bytes.Buffer{}
zl := zerolog.New(buf).Level(level).With().Timestamp().Logger()
return &Logger{zl: zl, level: level}, buf
}
func TestLogger_InfoOutput(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Info("测试消息")
output := buf.String()
if !strings.Contains(output, "测试消息") {
t.Errorf("输出应包含日志消息, got: %s", output)
}
if !strings.Contains(output, `"level":"info"`) {
t.Errorf("输出应包含 info 级别, got: %s", output)
}
}
func TestLogger_Infof(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Infof("你好 %s, 编号 %d", "世界", 42)
output := buf.String()
if !strings.Contains(output, "你好 世界, 编号 42") {
t.Errorf("格式化输出不正确, got: %s", output)
}
}
func TestLogger_DebugFilteredByLevel(t *testing.T) {
// Info 级别的 Logger 应过滤 Debug 消息
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Debug("这条不应出现")
if buf.Len() > 0 {
t.Errorf("Info 级别不应输出 Debug 消息, got: %s", buf.String())
}
}
func TestLogger_DebugVisibleAtDebugLevel(t *testing.T) {
logger, buf := newBufferLogger(zerolog.DebugLevel)
logger.Debug("调试信息")
if !strings.Contains(buf.String(), "调试信息") {
t.Errorf("Debug 级别应输出 Debug 消息, got: %s", buf.String())
}
}
func TestLogger_TraceFilteredByInfo(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Trace("跟踪信息")
if buf.Len() > 0 {
t.Errorf("Info 级别不应输出 Trace 消息, got: %s", buf.String())
}
}
func TestLogger_WarnOutput(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Warn("警告信息")
output := buf.String()
if !strings.Contains(output, "警告信息") {
t.Errorf("输出应包含警告消息, got: %s", output)
}
if !strings.Contains(output, `"level":"warn"`) {
t.Errorf("输出应包含 warn 级别, got: %s", output)
}
}
func TestLogger_ErrorOutput(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Error("错误信息")
output := buf.String()
if !strings.Contains(output, "错误信息") {
t.Errorf("输出应包含错误消息, got: %s", output)
}
if !strings.Contains(output, `"level":"error"`) {
t.Errorf("输出应包含 error 级别, got: %s", output)
}
}
func TestLogger_Errorf(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Errorf("文件 %s 失败: %d", "test.txt", 404)
if !strings.Contains(buf.String(), "文件 test.txt 失败: 404") {
t.Errorf("格式化错误输出不正确, got: %s", buf.String())
}
}
func TestLogger_Warnf(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.Warnf("版本 %s 已废弃", "v0.1.0")
if !strings.Contains(buf.String(), "版本 v0.1.0 已废弃") {
t.Errorf("格式化警告输出不正确, got: %s", buf.String())
}
}
func TestLogger_Tracef(t *testing.T) {
logger, buf := newBufferLogger(zerolog.TraceLevel)
logger.Tracef("进入函数 %s", "Scan")
if !strings.Contains(buf.String(), "进入函数 Scan") {
t.Errorf("格式化跟踪输出不正确, got: %s", buf.String())
}
}
func TestLogger_Debugf(t *testing.T) {
logger, buf := newBufferLogger(zerolog.DebugLevel)
logger.Debugf("变量值: %v", map[string]int{"a": 1})
if !strings.Contains(buf.String(), "变量值:") {
t.Errorf("格式化调试输出不正确, got: %s", buf.String())
}
}
// ==================== 结构化字段测试 ====================
func TestLogger_WithField(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.WithField("collector", "go").Msg("扫描完成")
output := buf.String()
if !strings.Contains(output, "collector") {
t.Errorf("输出应包含字段名 collector, got: %s", output)
}
if !strings.Contains(output, "扫描完成") {
t.Errorf("输出应包含消息, got: %s", output)
}
}
func TestLogger_WithError(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
testErr := os.ErrNotExist
logger.WithError(testErr).Msg("操作失败")
output := buf.String()
if !strings.Contains(output, "error") {
t.Errorf("输出应包含 error 字段, got: %s", output)
}
if !strings.Contains(output, "操作失败") {
t.Errorf("输出应包含消息, got: %s", output)
}
}
func TestLogger_WithDuration(t *testing.T) {
logger, buf := newBufferLogger(zerolog.InfoLevel)
logger.WithDuration(3 * time.Second).Msg("处理完成")
output := buf.String()
if !strings.Contains(output, "duration") {
t.Errorf("输出应包含 duration 字段, got: %s", output)
}
if !strings.Contains(output, "处理完成") {
t.Errorf("输出应包含消息, got: %s", output)
}
}
// ==================== 工具方法测试 ====================
func TestLogger_IsDebug(t *testing.T) {
tests := []struct {
name string
level zerolog.Level
expected bool
}{
{"TraceLevel", zerolog.TraceLevel, true},
{"DebugLevel", zerolog.DebugLevel, true},
{"InfoLevel", zerolog.InfoLevel, false},
{"WarnLevel", zerolog.WarnLevel, false},
{"ErrorLevel", zerolog.ErrorLevel, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := &Logger{level: tt.level, zl: zerolog.Nop()}
if logger.IsDebug() != tt.expected {
t.Errorf("IsDebug() 应为 %v (level=%s)", tt.expected, tt.level)
}
})
}
}
func TestLogger_IsQuiet(t *testing.T) {
logger := &Logger{quiet: true, zl: zerolog.Nop()}
if !logger.IsQuiet() {
t.Error("IsQuiet() 应返回 true")
}
logger2 := &Logger{quiet: false, zl: zerolog.Nop()}
if logger2.IsQuiet() {
t.Error("IsQuiet() 应返回 false")
}
}
func TestLogger_IsVerbose(t *testing.T) {
logger := &Logger{verbose: true, zl: zerolog.Nop()}
if !logger.IsVerbose() {
t.Error("IsVerbose() 应返回 true")
}
logger2 := &Logger{verbose: false, zl: zerolog.Nop()}
if logger2.IsVerbose() {
t.Error("IsVerbose() 应返回 false")
}
}
// ==================== Close 测试 ====================
func TestLogger_Close_WithFile(t *testing.T) {
tmpDir := t.TempDir()
logPath := filepath.Join(tmpDir, "close_test.log")
logger, err := New(Options{LogFile: logPath, Quiet: true})
if err != nil {
t.Fatalf("不应报错, got: %v", err)
}
// 写一条日志确保文件可用
logger.Info("关闭前的日志")
// Close 不应 panic
logger.Close()
// 关闭后文件不可写(已关闭)
_, writeErr := logger.logFile.WriteString("test")
if writeErr == nil {
t.Error("文件关闭后写入应报错")
}
}
func TestLogger_Close_WithoutFile(t *testing.T) {
logger, _ := New(Options{Quiet: true})
// 没有日志文件时 Close 不应 panic
logger.Close()
}
// ==================== 日志文件写入测试 ====================
func TestLogger_FileOutput(t *testing.T) {
tmpDir := t.TempDir()
logPath := filepath.Join(tmpDir, "output_test.log")
logger, err := New(Options{LogFile: logPath, Quiet: true})
if err != nil {
t.Fatalf("不应报错, got: %v", err)
}
logger.Info("写入文件的消息")
logger.Warn("警告消息")
logger.Close()
// 读取文件内容
content, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("读取日志文件失败: %v", err)
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
if len(lines) < 2 {
t.Fatalf("日志文件应至少有 2 行, got %d 行: %s", len(lines), string(content))
}
// 验证是合法的 JSON 格式(文件输出为 JSON
for i, line := range lines {
var entry map[string]any
if err := json.Unmarshal([]byte(line), &entry); err != nil {
t.Errorf("第 %d 行不是合法 JSON: %s", i+1, line)
}
}
}
// ==================== 级别过滤综合测试 ====================
func TestLogger_LevelFiltering(t *testing.T) {
tests := []struct {
name string
logLevel zerolog.Level
writeFunc func(l *Logger)
shouldLog bool
}{
{"Error级别写Info", zerolog.ErrorLevel, func(l *Logger) { l.Info("msg") }, false},
{"Error级别写Error", zerolog.ErrorLevel, func(l *Logger) { l.Error("msg") }, true},
{"Error级别写Warn", zerolog.ErrorLevel, func(l *Logger) { l.Warn("msg") }, false},
{"Warn级别写Warn", zerolog.WarnLevel, func(l *Logger) { l.Warn("msg") }, true},
{"Warn级别写Info", zerolog.WarnLevel, func(l *Logger) { l.Info("msg") }, false},
{"Debug级别写Trace", zerolog.DebugLevel, func(l *Logger) { l.Trace("msg") }, false},
{"Trace级别写Trace", zerolog.TraceLevel, func(l *Logger) { l.Trace("msg") }, true},
{"Info级别写Info", zerolog.InfoLevel, func(l *Logger) { l.Info("msg") }, true},
{"Info级别写Error", zerolog.InfoLevel, func(l *Logger) { l.Error("msg") }, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, buf := newBufferLogger(tt.logLevel)
tt.writeFunc(logger)
hasOutput := buf.Len() > 0
if hasOutput != tt.shouldLog {
t.Errorf("shouldLog=%v, but hasOutput=%v (output: %s)",
tt.shouldLog, hasOutput, buf.String())
}
})
}
}