Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: testing: Share logger between test code and application code #69809

Closed
palsivertsen opened this issue Oct 8, 2024 · 3 comments
Closed
Labels
Milestone

Comments

@palsivertsen
Copy link

Proposal Details

When working with failing tests it is useful to inspect logs from both the test code and the application code. The testing.T type (among other types) exports a method, Log(...any), that one can use to log during tests. The log output is formatted in such a way that it is easy to see which test it belongs to.

The application typically uses the log or log/slog package to do its logging with outputs to os.Stderr. These logs are separate from the test logs, making it hard to correlate to specific tests.

This program highlights the issue:

package main

import (
	"log"
	"testing"
)

func Log(msg string) {
	log.Println(msg)
}

func TestLog(t *testing.T) {
	t.Log("test start")
	Log("hello world")
	t.Log("test end")
	t.Fail()
}

Test output (varies by environment):

2024/10/08 13:19:47 hello world
--- FAIL: TestLog (0.00s)
    example1_test.go:13: test start
    example1_test.go:15: test end
FAIL
FAIL	command-line-arguments	0.001s
FAIL

Note how "hello world" is the first thing that is printed and it's outside the context of the test. The output can vary by environment, go playground typically outputs in desired order.

A solution to this problem is to share a common logger between test and application code. I implemented a wrapper around a .Log(...any) interface to highlight the desired effect. A more robust implementation should probably implement the slog.Handler interface.

Play

Code
package main

import (
	"log/slog"
	"os"
	"testing"
)

func TestILogStuff_LogThis(t *testing.T) {
	tests := map[string]struct {
		newLogger func(tb testing.TB) *slog.Logger
	}{
		"text handler": {
			newLogger: func(tb testing.TB) *slog.Logger { return slog.New(slog.NewTextHandler(os.Stderr, nil)) },
		},
		"text handler with testing.TB writer": {
			newLogger: func(tb testing.TB) *slog.Logger { return slog.New(slog.NewTextHandler(testingTBWriter{tb: tb}, nil)) },
		},
	}
	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			logger := tt.newLogger(t).
				With("test name", t.Name())

			unit := Example{
				Logger: logger.With("source", "the unit being tested"),
			}

			t.Log("test start")
			unit.Log("hello world!")
			t.Log("test end")

			t.Fail()
		})
	}
}

type Example struct {
	Logger *slog.Logger
}

func (l *Example) Log(msg string) {
	l.Logger.Info(msg)
}

// testingTBWriter is a naive io.Writer implementation that writes to tb.Log.
// This implementation is intended to demonstrate log output in tests.
// A potential issue with this approach is that lines might not align with the
// bytes given to the Write function.
// A better approach would be to use a type that implements the slog.Handler
// interface and wraps a TestLogger.
type testingTBWriter struct {
	tb TestLogger
}

func (w testingTBWriter) Write(b []byte) (int, error) {
	w.tb.Log(string(b))
	return len(b), nil
}

type TestLogger interface {
	Log(...any)
}

Output (varies by environment):

time=2024-10-08T13:26:24.945+02:00 level=INFO msg="hello world!" "test name"=TestILogStuff_LogThis/text_handler source="the unit being tested"
--- FAIL: TestILogStuff_LogThis (0.00s)
    --- FAIL: TestILogStuff_LogThis/text_handler (0.00s)
        example_test.go:29: test start
        example_test.go:31: test end
    --- FAIL: TestILogStuff_LogThis/text_handler_with_testing.TB_writer (0.00s)
        example_test.go:29: test start
        example_test.go:57: time=2024-10-08T13:26:24.945+02:00 level=INFO msg="hello world!" "test name"=TestILogStuff_LogThis/text_handler_with_testing.TB_writer source="the unit being tested"
            
        example_test.go:31: test end
FAIL
FAIL	command-line-arguments	0.001s
FAIL
@gopherbot gopherbot added this to the Proposal milestone Oct 8, 2024
@mateusz834
Copy link
Member

mateusz834 commented Oct 8, 2024

A solution to this problem is to share a common logger between test and application code.

I always use the -v flag, see:

$ go test . -v
=== RUN   TestLog
    main_test.go:13: test start
2024/10/08 14:02:12 hello world
    main_test.go:15: test end
--- FAIL: TestLog (0.00s)

EDIT: actually this is: #24929

@seankhliao
Copy link
Member

Duplicate of #59928

@seankhliao seankhliao marked this as a duplicate of #59928 Oct 8, 2024
@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Oct 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants