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:待测试的 Handlerresults:返回函数,返回[]map[string]any,每个 map 对应一次 Logger 输出方法的调用
返回值:
- 如果发现错误,返回通过
errors.Join组合的多个错误 - 如果没有错误,返回
nil
Handler 要求:
- Handler 应该启用 Info 及以上级别
- 应该正确处理标准键:
slog.TimeKey、slog.LevelKey、slog.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 版本 | 作用 |
|---|---|---|
Run | 1.22+ | 在子测试中运行 Handler 测试 |
TestHandler | 1.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 方法
- 时间处理(包括零时间)
使用建议:
- 使用
Run函数进行完整的测试套件 - 正确解析 Handler 的输出
- 处理故意丢弃的属性
- 确保 Handler 启用适当的级别
- 使用标准键
典型用法:
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 规范,并正确处理各种边界情况。