Skip to content

Commit

Permalink
zaptest: Add testing.TB compatible logger
Browse files Browse the repository at this point in the history
This adds `zaptest.NewTestLogger(testing.TB) *zap.Logger` and a
level-override variant of it. This will enable us to use non-nop loggers
from tests without spamming the output.

These loggers will not print any output unless the test failed or
`go test -v` was used.
  • Loading branch information
abhinav committed Oct 26, 2017
1 parent fdad3eb commit 6636ecf
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 0 deletions.
87 changes: 87 additions & 0 deletions zaptest/test_logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package zaptest

import (
"testing"

"go.uber.org/zap/zapcore"
)

// NewTestLogger builds a new Core that logs all messages to the given
// testing.TB.
//
// Use this with a *testing.T or *testing.B to get logs which get printed only
// if a test fails or if you ran go test -v.
//
// logger := zap.New(NewTestLogger(t))
func NewTestLogger(t testing.TB) zapcore.Core {
return NewTestLoggerAt(t, zapcore.DebugLevel)
}

// NewTestLoggerAt builds a new Core that logs messages to the given
// testing.TB if the given LevelEnabler allows it.
//
// Use this with a *testing.T or *testing.B to get logs which get printed only
// if a test fails or if you ran go test -v.
//
// logger := zap.New(NewTestLoggerAt(t, zap.InfoLevel))
func NewTestLoggerAt(t testing.TB, enab zapcore.LevelEnabler) zapcore.Core {
return zapcore.NewCore(
zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
// EncoderConfig copied from zap.NewDevelopmentEncoderConfig.
// Can't use it directly because that would cause a cyclic import.
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
MessageKey: "M",
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}),
testingWriter{t},
enab,
)
}

// testingWriter is a WriteSyncer that writes to the given testing.TB.
type testingWriter struct{ t testing.TB }

func (w testingWriter) Write(p []byte) (n int, err error) {
s := string(p)

// Strip trailing newline because t.Log always adds one.
if s[len(s)-1] == '\n' {
s = s[:len(s)-1]
}

// Note: t.Log is safe for concurrent use.
w.t.Log(s)
return len(p), nil
}

func (w testingWriter) Sync() error {
return nil
}
99 changes: 99 additions & 0 deletions zaptest/test_logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package zaptest

import (
"errors"
"fmt"
"strings"
"sync"
"testing"

"go.uber.org/zap"

"github.com/stretchr/testify/assert"
)

func TestTestLoggerIncludesDebug(t *testing.T) {
ts := newTestLogSpy(t)
log := zap.New(NewTestLogger(ts))
log.Debug("calculating")
log.Info("finished calculating", zap.Int("answer", 42))

ts.AssertMessages(
"DEBUG calculating",
`INFO finished calculating {"answer": 42}`,
)
}

func TestTestLoggerAt(t *testing.T) {
ts := newTestLogSpy(t)
log := zap.New(NewTestLoggerAt(ts, zap.WarnLevel))

log.Info("received work order")
log.Debug("starting work")
log.Warn("work may fail")
log.Error("work failed", zap.Error(errors.New("great sadness")))

assert.Panics(t, func() {
log.Panic("failed to do work")
}, "log.Panic should panic")

ts.AssertMessages(
"WARN work may fail",
`ERROR work failed {"error": "great sadness"}`,
"PANIC failed to do work",
)
}

// testLogSpy is a testing.TB that captures logged messages.
type testLogSpy struct {
testing.TB

mu sync.Mutex
Messages []string
}

func newTestLogSpy(t testing.TB) *testLogSpy {
return &testLogSpy{TB: t}
}

func (t *testLogSpy) Log(args ...interface{}) {
// Log messages are in the format,
//
// 2017-10-27T13:03:01.000-0700 DEBUG your message here {data here}
//
// We strip the first part of these messages because we can't really test
// for the timestamp from these tests.
m := fmt.Sprint(args...)
m = m[strings.IndexByte(m, '\t')+1:]

// t.Log should be thread-safe.
t.mu.Lock()
t.Messages = append(t.Messages, m)
t.mu.Unlock()

t.TB.Log(args...)
}

func (t *testLogSpy) AssertMessages(msgs ...string) {
assert.Equal(t, msgs, t.Messages)
}

0 comments on commit 6636ecf

Please sign in to comment.