Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

testing/slogtest 包详解

概述

testing/slogtest 包实现了支持测试 log/slog.Handler 实现的功能。它提供了一套完整的测试工具,用于验证自定义的 slog Handler 是否正确实现了所有必需的功能。

主要用途

  • 测试自定义 slog.Handler 实现
  • 验证 Handler 是否正确处理属性、组、上下文等
  • 确保 Handler 符合 slog 规范

Go 版本要求

  • TestHandler:Go 1.21+
  • Run:Go 1.22+

包导入

import "testing/slogtest"

函数详解(按 A-Z 分层归类)

R

Run

func Run(t *testing.T, newHandler func(*testing.T) slog.Handler, result func(*testing.T) map[string]any)

作用:在子测试中运行测试用例来练习 slog.Handler

参数说明

  • t:测试上下文
  • newHandler:工厂函数,用于创建待测试的 Handler 实例
  • result:获取结果的函数

特点

  • TestHandler 使用相同的测试用例
  • 每个测试用例在独立的子测试中运行
  • 自动调用 t.Error 报告失败的测试

示例

package myhandler_test

import (
    "log/slog"
    "testing"
    "testing/slogtest"
)

func TestMyHandler(t *testing.T) {
    slogtest.Run(t, func(t *testing.T) slog.Handler {
        // 创建新的 Handler 实例
        return NewMyHandler(t.TempDir())
    }, func(t *testing.T) map[string]any {
        // 返回测试结果
        return getResult(t)
    })
}

T

TestHandler

func TestHandler(h slog.Handler, results func() []map[string]any) error

作用:测试 slog.Handler 的实现是否正确

参数说明

  • h:待测试的 Handler
  • results:返回函数,返回 []map[string]any,每个 map 对应一次 Logger 输出方法的调用

返回值

  • 如果发现错误,返回通过 errors.Join 组合的多个错误
  • 如果没有错误,返回 nil

Handler 要求

  • Handler 应该启用 Info 及以上级别
  • 应该正确处理标准键:slog.TimeKeyslog.LevelKeyslog.MessageKey
  • 每个输出组应该表示为嵌套的 map[string]any

results 函数要求

  • 返回 []map[string]any 切片
  • 每个 map 对应一次 Logger 输出方法调用
  • Map 的键和值应该对应 Handler 输出的键和值
  • 如果 Handler 故意丢弃某个属性,results 函数应该检查其缺失并在返回的 map 中添加它

示例

package myhandler_test

import (
    "bytes"
    "encoding/json"
    "log/slog"
    "testing/slogtest"
)

func TestMyHandler(t *testing.T) {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)
    
    // 解析结果的函数
    results := func() []map[string]any {
        var ms []map[string]any
        for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            if err := json.Unmarshal(line, &m); err != nil {
                panic(err)
            }
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

类型详解

testing/slogtest 包不导出任何类型,所有功能通过函数提供。

典型示例

1. 测试 JSON Handler

package slogtest_test

import (
    "bytes"
    "encoding/json"
    "log"
    "log/slog"
    "testing/slogtest"
)

func ExampleJSONHandler() {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)
    
    results := func() []map[string]any {
        var ms []map[string]any
        for line := range bytes.SplitSeq(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            if err := json.Unmarshal(line, &m); err != nil {
                panic(err)
            }
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        log.Fatal(err)
    }
}

2. 测试 Text Handler

package slogtest_test

import (
    "bufio"
    "bytes"
    "log/slog"
    "strings"
    "testing/slogtest"
)

func TestTextHandler(t *testing.T) {
    var buf bytes.Buffer
    h := slog.NewTextHandler(&buf, nil)
    
    results := func() []map[string]any {
        var ms []map[string]any
        scanner := bufio.NewScanner(&buf)
        for scanner.Scan() {
            line := scanner.Text()
            // 解析 key=value 格式
            m := parseTextLine(line)
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

func parseTextLine(line string) map[string]any {
    m := make(map[string]any)
    // 简化的解析逻辑
    parts := strings.Split(line, " ")
    for _, part := range parts {
        kv := strings.SplitN(part, "=", 2)
        if len(kv) == 2 {
            m[kv[0]] = kv[1]
        }
    }
    return m
}

3. 测试自定义 Handler

package myhandler

import (
    "io"
    "log/slog"
)

// MyHandler 是自定义 Handler
type MyHandler struct {
    w io.Writer
}

func NewMyHandler(w io.Writer) *MyHandler {
    return &MyHandler{w: w}
}

func (h *MyHandler) Enabled(context.Context, slog.Level) bool {
    return true
}

func (h *MyHandler) Handle(ctx context.Context, r slog.Record) error {
    // 实现日志处理逻辑
    return nil
}

func (h *MyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    // 实现属性附加逻辑
    return h
}

func (h *MyHandler) WithGroup(name string) slog.Handler {
    // 实现组处理逻辑
    return h
}

4. 测试带属性的 Handler

package slogtest_test

import (
    "bytes"
    "encoding/json"
    "log/slog"
    "testing/slogtest"
)

func TestHandlerWithAttrs(t *testing.T) {
    var buf bytes.Buffer
    // 创建带预定义属性的 Handler
    h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
        AddSource: true,
        Level:     slog.LevelDebug,
    })
    
    results := func() []map[string]any {
        var ms []map[string]any
        for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            json.Unmarshal(line, &m)
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

5. 测试组的处理

package slogtest_test

import (
    "bytes"
    "encoding/json"
    "log/slog"
    "testing/slogtest"
)

func TestHandlerGroups(t *testing.T) {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)
    
    logger := slog.New(h)
    
    // 测试组的处理
    logger.Info("message",
        "a", "b",
        slog.Group("G",
            slog.String("c", "d"),
        ),
        "e", "f",
    )
    
    results := func() []map[string]any {
        var ms []map[string]any
        for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            json.Unmarshal(line, &m)
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

6. 使用 Run 函数测试

package myhandler_test

import (
    "log/slog"
    "testing"
    "testing/slogtest"
)

func TestMyHandlerWithRun(t *testing.T) {
    slogtest.Run(t,
        func(t *testing.T) slog.Handler {
            // 每个子测试创建新的 Handler 实例
            return NewMyHandler(t.TempDir())
        },
        func(t *testing.T) map[string]any {
            // 返回当前测试的结果
            return getCurrentResult(t)
        },
    )
}

7. 测试空属性处理

package slogtest_test

import (
    "bytes"
    "encoding/json"
    "log/slog"
    "testing/slogtest"
)

func TestHandlerEmptyAttrs(t *testing.T) {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)
    
    logger := slog.New(h)
    
    // 测试空属性的处理
    logger.Info("msg",
        "a", "b",
        "", nil, // 空键应该被忽略
        "c", "d",
    )
    
    results := func() []map[string]any {
        var ms []map[string]any
        for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            json.Unmarshal(line, &m)
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

8. 测试时间处理

package slogtest_test

import (
    "bytes"
    "encoding/json"
    "log/slog"
    "testing/slogtest"
    "time"
)

func TestHandlerTime(t *testing.T) {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)
    
    logger := slog.New(h)
    
    // 测试零时间的处理
    logger.Info("msg", "k", "v")
    
    results := func() []map[string]any {
        var ms []map[string]any
        for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
            if len(line) == 0 {
                continue
            }
            var m map[string]any
            json.Unmarshal(line, &m)
            ms = append(ms, m)
        }
        return ms
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

最佳实践

1. 使用 Run 函数进行完整测试

func TestMyHandler(t *testing.T) {
    slogtest.Run(t,
        func(t *testing.T) slog.Handler {
            return NewMyHandler()
        },
        func(t *testing.T) map[string]any {
            return getResult(t)
        },
    )
}

2. 正确解析 Handler 输出

results := func() []map[string]any {
    var ms []map[string]any
    for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) {
        if len(line) == 0 {
            continue
        }
        var m map[string]any
        json.Unmarshal(line, &m)
        ms = append(ms, m)
    }
    return ms
}

3. 处理故意丢弃的属性

results := func() []map[string]any {
    var ms []map[string]any
    // 解析输出
    for _, line := range lines {
        var m map[string]any
        json.Unmarshal(line, &m)
        
        // 如果 Handler 故意丢弃某个属性,检查并添加
        if shouldHaveDroppedAttr {
            m["dropped_attr"] = "expected_value"
        }
        
        ms = append(ms, m)
    }
    return ms
}

4. 使用标准键

const (
    TimeKey    = "time"
    LevelKey   = "level"
    MessageKey = "msg"
    SourceKey  = "source"
)

// 确保 Handler 正确处理这些键

5. 测试所有级别

func TestAllLevels(t *testing.T) {
    h := NewMyHandler()
    
    logger := slog.New(h)
    
    logger.Debug("debug message")
    logger.Info("info message")
    logger.Warn("warn message")
    logger.Error("error message")
    
    // 验证所有级别都被正确处理
}

与其他包配合

log/slog 包

import (
    "log/slog"
    "testing/slogtest"
)

func TestHandler(t *testing.T) {
    h := slog.NewJSONHandler(os.Stdout, nil)
    slogtest.TestHandler(h, results)
}

encoding/json 包

import (
    "encoding/json"
    "testing/slogtest"
)

results := func() []map[string]any {
    var m map[string]any
    json.Unmarshal(data, &m)
    return []map[string]any{m}
}

bytes 包

import (
    "bytes"
    "testing/slogtest"
)

var buf bytes.Buffer
h := slog.NewJSONHandler(&buf, nil)

注意事项

1. Handler 级别要求

Handler 应该启用 Info 及以上级别,否则某些测试可能会失败。

h := slog.NewJSONHandler(&buf, &slog.HandlerOptions{
    Level: slog.LevelInfo,
})

2. 标准键的使用

确保 Handler 正确使用标准键:

  • slog.TimeKey (“time”)
  • slog.LevelKey (“level”)
  • slog.MessageKey (“msg”)

3. 组的表示

每个输出组应该表示为嵌套的 map[string]any

// 正确的组表示
{
    "time": "2024-01-01T00:00:00Z",
    "level": "INFO",
    "msg": "message",
    "group": {
        "key": "value"
    }
}

4. 空属性的处理

Handler 应该正确处理空属性:

  • 空键应该被忽略
  • nil 值应该被正确处理

5. 时间的处理

  • Handler 应该正确处理零时间
  • 零时间应该被忽略

快速参考

函数速查表

函数Go 版本作用
Run1.22+在子测试中运行 Handler 测试
TestHandler1.21+测试 Handler 实现

TestHandler 测试用例

测试用例说明
built-ins测试标准键 (TimeKey, LevelKey, MessageKey)
attrs测试属性传递
empty-attr测试空属性处理
zero-time测试零时间处理
WithAttrs测试 WithAttrs 方法
groups测试组属性
empty-group测试空组
inline-group测试内联组
WithGroup测试 WithGroup 方法
multi-With测试多次 With 调用
empty-group-record测试记录中的空组

标准键

slog.TimeKey    // "time"
slog.LevelKey   // "level"
slog.MessageKey // "msg"
slog.SourceKey  // "source"

常见模式

// 基本测试模式
func TestHandler(t *testing.T) {
    var buf bytes.Buffer
    h := slog.NewJSONHandler(&buf, nil)
    
    results := func() []map[string]any {
        // 解析输出
    }
    
    err := slogtest.TestHandler(h, results)
    if err != nil {
        t.Fatal(err)
    }
}

// 使用 Run 函数
func TestHandlerWithRun(t *testing.T) {
    slogtest.Run(t,
        func(t *testing.T) slog.Handler {
            return NewHandler()
        },
        func(t *testing.T) map[string]any {
            return getResult(t)
        },
    )
}

总结

testing/slogtest 包提供了完整的工具来测试 slog.Handler 实现:

核心功能

  • TestHandler:基础测试函数
  • Run:在子测试中运行测试(Go 1.22+)

测试覆盖

  • 标准键处理(TimeKey, LevelKey, MessageKey)
  • 属性传递和处理
  • 空属性和空组处理
  • 组属性处理
  • WithAttrs 和 WithGroup 方法
  • 时间处理(包括零时间)

使用建议

  1. 使用 Run 函数进行完整的测试套件
  2. 正确解析 Handler 的输出
  3. 处理故意丢弃的属性
  4. 确保 Handler 启用适当的级别
  5. 使用标准键

典型用法

func TestMyHandler(t *testing.T) {
    var buf bytes.Buffer
    h := NewMyHandler(&buf)
    
    results := func() []map[string]any {
        // 解析 buf 中的输出
    }
    
    if err := slogtest.TestHandler(h, results); err != nil {
        t.Fatal(err)
    }
}

通过 testing/slogtest 包,可以确保自定义 Handler 实现符合 slog 规范,并正确处理各种边界情况。