or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdfields.mdindex.mdintegrations.mdlogger.mdtesting.mdzapcore.md
tile.json

testing.mddocs/

Testing Utilities

Zap provides dedicated testing packages to facilitate testing code that uses zap loggers: zaptest for creating test loggers and zaptest/observer for inspecting logged entries.

Package: zaptest

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

The zaptest package provides utilities for creating loggers in tests that write to the test output.

Creating Test Loggers

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

Create a logger that writes to the test's output. The logger is configured to:

  • Write logs to t.Logf
  • Use development-mode encoding (human-readable)
  • Enable all log levels by default

Parameters:

  • t: Test instance (testing.T or testing.B)
  • opts: Optional configuration options

TestingT Interface

type TestingT interface {
    Logf(format string, args ...interface{})
}

Subset of testing.TB implemented by both *testing.T and *testing.B.

Logger Options

type LoggerOption interface {
    // unexported method
}

func Level(enab zapcore.LevelEnabler) LoggerOption
func WrapOptions(zapOpts ...zap.Option) LoggerOption
  • Level: Set the minimum enabled log level for the test logger
  • WrapOptions: Apply standard zap.Option values to the test logger

Usage Examples

Basic Test Logger

func TestSomething(t *testing.T) {
    logger := zaptest.NewLogger(t)
    defer logger.Sync()

    logger.Info("test started")

    // Test code that uses logger
    result := processWithLogging(logger, input)

    logger.Info("test completed", zap.Any("result", result))
    assert.Equal(t, expected, result)
}

Test Logger with Custom Level

func TestDebugMode(t *testing.T) {
    logger := zaptest.NewLogger(t, zaptest.Level(zapcore.DebugLevel))

    logger.Debug("this will be logged")
    // Test debug-level functionality
}

Test Logger with Additional Options

func TestWithStacktraces(t *testing.T) {
    logger := zaptest.NewLogger(t,
        zaptest.WrapOptions(
            zap.AddCaller(),
            zap.AddStacktrace(zapcore.WarnLevel),
        ),
    )

    logger.Warn("warning with stacktrace")
}

Testing Writers

type TestingWriter struct {
    // unexported fields
}

func NewTestingWriter(t TestingT) *TestingWriter

Create an io.Writer that writes to test output. Implements io.Writer and splits output on newlines.

func (w *TestingWriter) Write(p []byte) (n int, err error)

Timeout Writer

type TimeoutWriter struct {
    // unexported fields
}

func NewTimeoutWriter(t TestingT, timeout time.Duration) *TimeoutWriter

func (w *TimeoutWriter) Write(p []byte) (n int, err error)
func (w *TimeoutWriter) Stop() error

Writer that fails the test if no writes occur within the timeout period. Useful for detecting hung tests.

Usage Example

func TestWithTimeout(t *testing.T) {
    writer := zaptest.NewTimeoutWriter(t, 1*time.Second)
    defer writer.Stop()

    logger := zap.New(
        zapcore.NewCore(
            zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
            zapcore.AddSync(writer),
            zapcore.DebugLevel,
        ),
    )

    // Test will fail if no logs written within 1 second
    logger.Info("keeping test alive")
}

Package: zaptest/observer

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

The observer package provides facilities for inspecting logged entries during tests, enabling assertions about log output.

Creating Observed Loggers

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

Create a zapcore.Core that captures all log entries and an ObservedLogs instance for inspecting them. The core should be wrapped in a zap.Logger using zap.New().

Parameters:

  • enab: Level enabler (e.g., zapcore.DebugLevel to capture all levels)

Returns:

  • zapcore.Core: Core to use with zap.New()
  • *ObservedLogs: Observer for inspecting logged entries

ObservedLogs Type

type ObservedLogs struct {
    // unexported fields
}

Collection of logged entries with filtering and inspection methods.

Accessing Entries

func (o *ObservedLogs) All() []LoggedEntry
func (o *ObservedLogs) TakeAll() []LoggedEntry
func (o *ObservedLogs) AllUntimed() []LoggedEntry
func (o *ObservedLogs) Len() int
  • All: Return all logged entries (non-destructive)
  • TakeAll: Return all logged entries and clear the buffer
  • AllUntimed: Return entries with zeroed timestamps (for deterministic comparison)
  • Len: Number of logged entries

Filtering Entries

func (o *ObservedLogs) FilterMessage(msg string) *ObservedLogs
func (o *ObservedLogs) FilterMessageSnippet(snippet string) *ObservedLogs
func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs
func (o *ObservedLogs) FilterFieldKey(key string) *ObservedLogs
func (o *ObservedLogs) FilterLevelExact(lvl zapcore.Level) *ObservedLogs
func (o *ObservedLogs) FilterLoggerName(name string) *ObservedLogs
func (o *ObservedLogs) Filter(f func(LoggedEntry) bool) *ObservedLogs

All filter methods return a new *ObservedLogs containing only entries matching the filter. Original ObservedLogs is unchanged.

  • FilterMessage: Entries with exact message match
  • FilterMessageSnippet: Entries containing message substring
  • FilterField: Entries containing specific field
  • FilterFieldKey: Entries containing field with specific key
  • FilterLevelExact: Entries at exact log level
  • FilterLoggerName: Entries logged by logger with specified name
  • Filter: Custom filter function

LoggedEntry Type

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

Captured log entry with metadata and context fields.

Fields:

  • Entry: Entry metadata (level, message, time, caller, stack)
  • Context: Context fields logged with the entry

LoggedEntry Methods

func (e LoggedEntry) ContextMap() map[string]interface{}
func (e LoggedEntry) CheckMessageStartsWith(want string) error
func (e LoggedEntry) CheckStackTrace(want string) error
  • ContextMap: Convert context fields to map (uses reflection, for tests only)
  • CheckMessageStartsWith: Validate message prefix
  • CheckStackTrace: Validate stack trace contents

Usage Examples

Basic Log Observation

func TestLogging(t *testing.T) {
    observedCore, logs := observer.New(zapcore.InfoLevel)
    logger := zap.New(observedCore)

    logger.Info("user logged in", zap.String("username", "alice"))
    logger.Warn("slow query", zap.Duration("latency", 500*time.Millisecond))

    // Assert log count
    assert.Equal(t, 2, logs.Len())

    // Inspect entries
    entries := logs.All()
    assert.Equal(t, "user logged in", entries[0].Message)
    assert.Equal(t, zapcore.InfoLevel, entries[0].Level)

    // Check context fields
    contextMap := entries[0].ContextMap()
    assert.Equal(t, "alice", contextMap["username"])
}

Filtering Entries

func TestErrorLogging(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    logger := zap.New(core)

    logger.Info("operation started")
    logger.Error("operation failed", zap.Error(errors.New("network error")))
    logger.Info("cleanup complete")

    // Filter to only errors
    errorLogs := logs.FilterLevelExact(zapcore.ErrorLevel)
    assert.Equal(t, 1, errorLogs.Len())

    // Filter by message
    failedLogs := logs.FilterMessageSnippet("failed")
    assert.Equal(t, 1, failedLogs.Len())

    // Filter by field key
    logsWithError := logs.FilterFieldKey("error")
    assert.Equal(t, 1, logsWithError.Len())
}

Custom Filtering

func TestSlowOperations(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    logger := zap.New(core)

    logger.Info("fast op", zap.Duration("duration", 10*time.Millisecond))
    logger.Info("slow op", zap.Duration("duration", 600*time.Millisecond))
    logger.Info("normal op", zap.Duration("duration", 100*time.Millisecond))

    // Custom filter for slow operations (>500ms)
    slowLogs := logs.Filter(func(entry observer.LoggedEntry) bool {
        contextMap := entry.ContextMap()
        if duration, ok := contextMap["duration"].(time.Duration); ok {
            return duration > 500*time.Millisecond
        }
        return false
    })

    assert.Equal(t, 1, slowLogs.Len())
    assert.Equal(t, "slow op", slowLogs.All()[0].Message)
}

Take Pattern for Incremental Testing

func TestIncrementalLogging(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    logger := zap.New(core)

    // First operation
    logger.Info("step 1")
    entries := logs.TakeAll()  // Take and clear
    assert.Equal(t, 1, len(entries))
    assert.Equal(t, "step 1", entries[0].Message)

    // Second operation
    logger.Info("step 2")
    entries = logs.TakeAll()  // Take and clear
    assert.Equal(t, 1, len(entries))
    assert.Equal(t, "step 2", entries[0].Message)

    // No logs remain
    assert.Equal(t, 0, logs.Len())
}

Validating Stack Traces

func TestStackTraces(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    logger := zap.New(core, zap.AddStacktrace(zapcore.ErrorLevel))

    logger.Error("critical error")

    entries := logs.FilterLevelExact(zapcore.ErrorLevel).All()
    assert.Equal(t, 1, len(entries))

    entry := entries[0]
    assert.NotEmpty(t, entry.Stack)

    // Check stack trace contains expected function
    err := entry.CheckStackTrace("TestStackTraces")
    assert.NoError(t, err)
}

Combined with Context Loggers

func TestContextLogging(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    logger := zap.New(core)

    // Create child logger with context
    requestLogger := logger.With(
        zap.String("request_id", "req-123"),
        zap.String("user", "alice"),
    )

    requestLogger.Info("processing request")
    requestLogger.Info("request complete")

    // All entries should have context fields
    entries := logs.All()
    for _, entry := range entries {
        contextMap := entry.ContextMap()
        assert.Equal(t, "req-123", contextMap["request_id"])
        assert.Equal(t, "alice", contextMap["user"])
    }
}

Best Practices

Use zaptest.NewLogger for Simple Tests

func TestSimple(t *testing.T) {
    logger := zaptest.NewLogger(t)
    // Test code runs, logs appear in test output
}

Use observer for Assertion Tests

func TestLogAssertions(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    logger := zap.New(core)

    // Run code under test
    myFunction(logger)

    // Assert expected logs
    assert.Equal(t, 1, logs.FilterMessage("expected message").Len())
}

Combine Both for Maximum Insight

func TestComprehensive(t *testing.T) {
    // Create observed core
    observedCore, logs := observer.New(zapcore.DebugLevel)

    // Create test writer
    testWriter := zaptest.NewTestingWriter(t)
    testCore := zapcore.NewCore(
        zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
        zapcore.AddSync(testWriter),
        zapcore.DebugLevel,
    )

    // Tee to both
    logger := zap.New(zapcore.NewTee(observedCore, testCore))

    // Run test - logs appear in test output AND can be inspected
    myFunction(logger)

    assert.Equal(t, 1, logs.FilterLevelExact(zapcore.ErrorLevel).Len())
}