This document covers testing utilities for verifying log output, including test loggers, in-memory observation, and buffer implementations.
The zaptest package provides utilities for testing log output.
import "go.uber.org/zap/zaptest"func NewLogger(t TestingT, opts ...LoggerOption) *zap.LoggerCreates a Logger that logs to the given testing.TB (e.g., *testing.T or *testing.B).
type TestingT interface {
Logf(string, ...interface{})
Errorf(string, ...interface{})
Fail()
Failed() bool
Name() string
FailNow()
}Subset of testing.T and testing.B interfaces, allowing zaptest to work with both.
type LoggerOption interface {
// Has unexported methods
}
// Control which messages are logged
func Level(enab zapcore.LevelEnabler) LoggerOption
// Add zap.Option's to the test Logger
func WrapOptions(zapOpts ...zap.Option) LoggerOptiontype TestingWriter struct {
// Has unexported fields
}
func NewTestingWriter(t TestingT) TestingWriterWriteSyncer that writes to the given testing.TB.
// Write bytes to testing.TB (implements io.Writer)
func (w TestingWriter) Write(p []byte) (n int, err error)
// Sync is a no-op (implements WriteSyncer)
func (w TestingWriter) Sync() error
// Return copy with markFailed behavior
func (w TestingWriter) WithMarkFailed(v bool) TestingWriter// Aliases for internal testing types
type Buffer = ztest.Buffer
type Discarder = ztest.Discarder
type FailWriter = ztest.FailWriter
type ShortWriter = ztest.ShortWriter
type Syncer = ztest.Syncer// Deprecated: scales sleep duration by $TEST_TIMEOUT_SCALE
func Sleep(base time.Duration)
// Deprecated: scales duration by $TEST_TIMEOUT_SCALE
func Timeout(base time.Duration) time.Durationimport (
"testing"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
)
func TestMyFunction(t *testing.T) {
// Create logger that writes to test output
logger := zaptest.NewLogger(t)
defer logger.Sync()
// Use logger in test
logger.Info("starting test")
result := MyFunction(logger)
logger.Info("test completed", zap.Any("result", result))
}func TestWithDebugLevel(t *testing.T) {
// Create debug-level test logger
logger := zaptest.NewLogger(t,
zaptest.Level(zap.DebugLevel),
)
logger.Debug("this will be logged")
logger.Info("so will this")
}
func TestWithZapOptions(t *testing.T) {
// Add standard zap options
logger := zaptest.NewLogger(t,
zaptest.WrapOptions(
zap.AddCaller(),
zap.AddStacktrace(zap.ErrorLevel),
),
)
logger.Error("error with caller and stack")
}func BenchmarkLogging(b *testing.B) {
logger := zaptest.NewLogger(b, zaptest.Level(zap.ErrorLevel))
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Info("benchmark log", zap.Int("iteration", i))
}
}The observer package provides in-memory log capture for assertions.
import "go.uber.org/zap/zaptest/observer"type ObservedLogs struct {
// Has unexported fields
}ObservedLogs is a concurrency-safe, ordered collection of observed logs.
func New(enab zapcore.LevelEnabler) (zapcore.Core, *ObservedLogs)Creates a Core that buffers logs in memory and returns the ObservedLogs for assertions.
// Get copy of all observed logs
func (o *ObservedLogs) All() []LoggedEntry
// Get copy of all logs with timestamps zeroed
func (o *ObservedLogs) AllUntimed() []LoggedEntry
// Get copy and clear observed logs
func (o *ObservedLogs) TakeAll() []LoggedEntry
// Get number of observed logs
func (o *ObservedLogs) Len() int// Filter logs with custom predicate
func (o *ObservedLogs) Filter(keep func(LoggedEntry) bool) *ObservedLogs
// Filter logs containing specific field
func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs
// Filter logs with specific field key
func (o *ObservedLogs) FilterFieldKey(key string) *ObservedLogs
// Filter logs at exact level
func (o *ObservedLogs) FilterLevelExact(level zapcore.Level) *ObservedLogs
// Filter logs from specific logger name
func (o *ObservedLogs) FilterLoggerName(name string) *ObservedLogs
// Filter logs with exact message
func (o *ObservedLogs) FilterMessage(msg string) *ObservedLogs
// Filter logs containing message snippet
func (o *ObservedLogs) FilterMessageSnippet(snippet string) *ObservedLogstype LoggedEntry struct {
zapcore.Entry
Context []zapcore.Field
}LoggedEntry is an encoding-agnostic representation of a log message.
// Convert Context fields to map
func (e LoggedEntry) ContextMap() map[string]interface{}import (
"testing"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)
func TestLogging(t *testing.T) {
// Create observer
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
// Generate logs
logger.Info("test message", zap.String("key", "value"))
logger.Error("error message")
// Assert on logs
if logs.Len() != 2 {
t.Errorf("expected 2 logs, got %d", logs.Len())
}
allLogs := logs.All()
if allLogs[0].Message != "test message" {
t.Errorf("unexpected message: %s", allLogs[0].Message)
}
}func TestFilteringLogs(t *testing.T) {
core, logs := observer.New(zapcore.DebugLevel)
logger := zap.New(core)
logger.Debug("debug message")
logger.Info("info message")
logger.Error("error message")
// Filter to only error logs
errorLogs := logs.FilterLevelExact(zapcore.ErrorLevel)
if errorLogs.Len() != 1 {
t.Errorf("expected 1 error log, got %d", errorLogs.Len())
}
// Filter by message
infoLogs := logs.FilterMessage("info message")
if infoLogs.Len() != 1 {
t.Errorf("expected 1 info log, got %d", infoLogs.Len())
}
}func TestFieldAssertion(t *testing.T) {
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
logger.Info("user action",
zap.String("username", "alice"),
zap.Int("user_id", 42),
)
// Filter by field
aliceLogs := logs.FilterField(zap.String("username", "alice"))
if aliceLogs.Len() != 1 {
t.Errorf("expected 1 log with username=alice")
}
// Check field values
entry := logs.All()[0]
contextMap := entry.ContextMap()
if contextMap["username"] != "alice" {
t.Errorf("expected username=alice")
}
if contextMap["user_id"] != int64(42) {
t.Errorf("expected user_id=42")
}
}func TestMessagePattern(t *testing.T) {
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
logger.Info("user alice logged in")
logger.Info("user bob logged in")
logger.Info("system started")
// Filter by message snippet
loginLogs := logs.FilterMessageSnippet("logged in")
if loginLogs.Len() != 2 {
t.Errorf("expected 2 login logs, got %d", loginLogs.Len())
}
}func TestNamedLoggers(t *testing.T) {
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
dbLogger := logger.Named("database")
apiLogger := logger.Named("api")
dbLogger.Info("query executed")
apiLogger.Info("request handled")
// Filter by logger name
dbLogs := logs.FilterLoggerName("database")
if dbLogs.Len() != 1 {
t.Errorf("expected 1 database log")
}
apiLogs := logs.FilterLoggerName("api")
if apiLogs.Len() != 1 {
t.Errorf("expected 1 api log")
}
}func TestTakePattern(t *testing.T) {
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
// First batch of logs
logger.Info("message 1")
logger.Info("message 2")
firstBatch := logs.TakeAll()
if len(firstBatch) != 2 {
t.Errorf("expected 2 logs in first batch")
}
// logs is now empty
if logs.Len() != 0 {
t.Errorf("expected 0 logs after TakeAll")
}
// Second batch
logger.Info("message 3")
secondBatch := logs.TakeAll()
if len(secondBatch) != 1 {
t.Errorf("expected 1 log in second batch")
}
}func TestComplexFiltering(t *testing.T) {
core, logs := observer.New(zapcore.DebugLevel)
logger := zap.New(core)
logger.Info("user alice logged in", zap.String("action", "login"))
logger.Info("user bob logged in", zap.String("action", "login"))
logger.Info("user alice updated profile", zap.String("action", "update"))
logger.Error("user bob failed", zap.String("action", "login"))
// Chain filters
aliceLoginLogs := logs.
FilterMessageSnippet("alice").
FilterFieldKey("action").
FilterLevelExact(zapcore.InfoLevel)
if aliceLoginLogs.Len() != 2 {
t.Errorf("expected 2 alice info logs with action field")
}
}func TestCustomPredicate(t *testing.T) {
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
logger.Info("short")
logger.Info("this is a longer message")
logger.Info("medium length")
// Filter with custom predicate
longMessages := logs.Filter(func(entry observer.LoggedEntry) bool {
return len(entry.Message) > 10
})
if longMessages.Len() != 2 {
t.Errorf("expected 2 long messages")
}
}The buffer package provides efficient byte buffer management.
import "go.uber.org/zap/buffer"type Buffer struct {
// Has unexported fields
}Buffer is a thin wrapper around byte slice, intended to be pooled.
func (b *Buffer) AppendBool(v bool)
func (b *Buffer) AppendByte(v byte)
func (b *Buffer) AppendBytes(v []byte)
func (b *Buffer) AppendFloat(f float64, bitSize int)
func (b *Buffer) AppendInt(i int64)
func (b *Buffer) AppendString(s string)
func (b *Buffer) AppendTime(t time.Time, layout string)
func (b *Buffer) AppendUint(i uint64)func (b *Buffer) Write(bs []byte) (int, error)
func (b *Buffer) WriteByte(v byte) error
func (b *Buffer) WriteString(s string) (int, error)// Get underlying byte slice
func (b *Buffer) Bytes() []byte
// Get buffer length
func (b *Buffer) Len() int
// Get buffer capacity
func (b *Buffer) Cap() int
// Reset buffer to empty
func (b *Buffer) Reset()
// Remove trailing newline
func (b *Buffer) TrimNewline()
// Get string copy
func (b *Buffer) String() string
// Return buffer to pool
func (b *Buffer) Free()type Pool struct {
// Has unexported fields
}
func NewPool() PoolType-safe wrapper around sync.Pool for Buffer objects.
// Get Buffer from pool (creates if necessary)
func (p Pool) Get() *Bufferimport "go.uber.org/zap/buffer"
// Create pool
pool := buffer.NewPool()
// Get buffer from pool
buf := pool.Get()
defer buf.Free() // Return to pool
// Use buffer
buf.AppendString("Hello, ")
buf.AppendString("world!")
// Get bytes
data := buf.Bytes()func encodeCustomFormat(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
buf := buffer.NewPool().Get()
buf.AppendString(entry.Level.String())
buf.AppendString(" ")
buf.AppendTime(entry.Time, time.RFC3339)
buf.AppendString(" ")
buf.AppendString(entry.Message)
for _, field := range fields {
buf.AppendString(" ")
buf.AppendString(field.Key)
buf.AppendString("=")
// Append field value...
}
buf.AppendByte('\n')
return buf, nil
}// Check that specific log was created
func assertLogExists(t *testing.T, logs *observer.ObservedLogs, level zapcore.Level, msg string) {
found := logs.FilterLevelExact(level).FilterMessage(msg)
if found.Len() == 0 {
t.Errorf("expected log not found: level=%s msg=%s", level, msg)
}
}
// Check that log contains field
func assertLogHasField(t *testing.T, logs *observer.ObservedLogs, key string, value interface{}) {
found := logs.FilterFieldKey(key)
if found.Len() == 0 {
t.Errorf("no logs found with field key: %s", key)
return
}
entry := found.All()[0]
contextMap := entry.ContextMap()
if contextMap[key] != value {
t.Errorf("field %s: expected %v, got %v", key, value, contextMap[key])
}
}
// Check log count
func assertLogCount(t *testing.T, logs *observer.ObservedLogs, expected int) {
if logs.Len() != expected {
t.Errorf("expected %d logs, got %d", expected, logs.Len())
}
}func TestMyHandler(t *testing.T) {
// Setup
core, logs := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
handler := NewHandler(logger)
// Execute
handler.Process(request)
// Assert
if logs.Len() == 0 {
t.Fatal("no logs produced")
}
processLogs := logs.FilterMessageSnippet("processing")
if processLogs.Len() != 1 {
t.Errorf("expected 1 processing log, got %d", processLogs.Len())
}
errorLogs := logs.FilterLevelExact(zapcore.ErrorLevel)
if errorLogs.Len() != 0 {
t.Errorf("unexpected errors: %v", errorLogs.All())
}
}func BenchmarkLogger(b *testing.B) {
logger := zaptest.NewLogger(b, zaptest.Level(zap.ErrorLevel))
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
logger.Info("benchmark message",
zap.String("key", "value"),
zap.Int("count", 42),
)
}
})
}
func BenchmarkObserver(b *testing.B) {
core, _ := observer.New(zapcore.InfoLevel)
logger := zap.New(core)
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Info("message", zap.Int("iteration", i))
}
}