or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

fields.mdindex.mdintegrations.mdlogger.mdtesting.mdzapcore.md
tile.json

testing.mddocs/

Testing Utilities API

This document covers testing utilities for verifying log output, including test loggers, in-memory observation, and buffer implementations.

zaptest Package

The zaptest package provides utilities for testing log output.

Package Import

import "go.uber.org/zap/zaptest"

Test Logger Construction

func NewLogger(t TestingT, opts ...LoggerOption) *zap.Logger

Creates a Logger that logs to the given testing.TB (e.g., *testing.T or *testing.B).

TestingT Interface

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.

Logger Options

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) LoggerOption

TestingWriter

type TestingWriter struct {
    // Has unexported fields
}

func NewTestingWriter(t TestingT) TestingWriter

WriteSyncer that writes to the given testing.TB.

TestingWriter Methods

// 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

Type Aliases

// 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 Functions

// 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.Duration

zaptest Usage Examples

Basic Test Logger

import (
    "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))
}

Test Logger with Options

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")
}

Benchmark with Test Logger

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))
    }
}

zaptest/observer Package

The observer package provides in-memory log capture for assertions.

Package Import

import "go.uber.org/zap/zaptest/observer"

ObservedLogs Type

type ObservedLogs struct {
    // Has unexported fields
}

ObservedLogs is a concurrency-safe, ordered collection of observed logs.

ObservedLogs Constructor

func New(enab zapcore.LevelEnabler) (zapcore.Core, *ObservedLogs)

Creates a Core that buffers logs in memory and returns the ObservedLogs for assertions.

ObservedLogs Methods

Retrieval Methods

// 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 Methods

// 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) *ObservedLogs

LoggedEntry Type

type LoggedEntry struct {
    zapcore.Entry
    Context []zapcore.Field
}

LoggedEntry is an encoding-agnostic representation of a log message.

LoggedEntry Methods

// Convert Context fields to map
func (e LoggedEntry) ContextMap() map[string]interface{}

observer Usage Examples

Basic Observation

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)
    }
}

Filtering Logs

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())
    }
}

Field Assertions

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")
    }
}

Message Pattern Matching

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())
    }
}

Named Logger Testing

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")
    }
}

Take and Reset Pattern

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")
    }
}

Complex Filter Chains

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")
    }
}

Custom Predicate Filtering

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")
    }
}

buffer Package

The buffer package provides efficient byte buffer management.

Package Import

import "go.uber.org/zap/buffer"

Buffer Type

type Buffer struct {
    // Has unexported fields
}

Buffer is a thin wrapper around byte slice, intended to be pooled.

Buffer Methods

Append Methods

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)

io.Writer Methods

func (b *Buffer) Write(bs []byte) (int, error)
func (b *Buffer) WriteByte(v byte) error
func (b *Buffer) WriteString(s string) (int, error)

Buffer Management

// 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()

Pool Type

type Pool struct {
    // Has unexported fields
}

func NewPool() Pool

Type-safe wrapper around sync.Pool for Buffer objects.

Pool Methods

// Get Buffer from pool (creates if necessary)
func (p Pool) Get() *Buffer

buffer Usage Examples

Using Buffer Pool

import "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()

Custom Encoder with Buffer

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
}

Testing Best Practices

When to Use Each Tool

  1. zaptest.NewLogger: Use for integration tests where you want to see logs in test output
  2. observer: Use for unit tests where you need to assert on specific log entries
  3. buffer.Pool: Use when implementing custom encoders or cores

Assertion Patterns

// 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())
    }
}

Common Test Patterns

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())
    }
}

Performance Testing

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))
    }
}