How to Connect slog.Logger with testing.T in Go

I’m working on a Go project where I need to bridge slog.Logger to testing.T’s logging system. I want to forward slog.Logger calls so they show as log messages on testing.T. I’ve created a custom log handler:

type TestLogger struct {
    t *testing.T
    failOnError bool
}

func (tl TestLogger) Handle(ctx context.Context, r slog.Record) error {
    tl.t.Helper()
    if r.Level >= slog.LevelError && tl.failOnError {
        tl.t.Errorf("Log level %v: %s", r.Level, r.Message)
    } else {
        tl.t.Logf("Log level %v: %s", r.Level, r.Message)
    }
    return nil
}

// Additional methods for handling attributes and groups can be implemented here

This solution helps catch errors early in tests by failing on error-level logs. I would appreciate feedback on how to better manage attributes and log groups while ensuring the overall test clarity.

I’ve used a similar approach in my projects, and it’s been quite effective. One thing that’s helped me a lot is implementing a buffer to capture logs during test execution. This way, you can analyze the entire log output after the test completes, which is super useful for debugging complex scenarios.

Here’s a quick example of how you might implement this:

type BufferedTestLogger struct {
    t      *testing.T
    buffer bytes.Buffer
}

func (btl *BufferedTestLogger) Handle(ctx context.Context, r slog.Record) error {
    btl.t.Helper()
    fmt.Fprintf(&btl.buffer, "[%s] %s\n", r.Level, r.Message)
    return nil
}

func (btl *BufferedTestLogger) Flush() {
    btl.t.Log(btl.buffer.String())
    btl.buffer.Reset()
}

You can then use this in your tests and call Flush() at the end to dump all logs. This approach has saved me countless hours when dealing with asynchronous operations or race conditions in tests. Just remember to call Flush() in defer statements to ensure logs are always output, even if the test panics.

Your approach looks solid. One thing to consider is handling structured logging more effectively. You could extend your TestLogger to capture and format attributes:

func (tl TestLogger) Handle(ctx context.Context, r slog.Record) error {
    tl.t.Helper()
    attrs := make([]string, 0, r.NumAttrs())
    r.Attrs(func(a slog.Attr) bool {
        attrs = append(attrs, fmt.Sprintf("%s=%v", a.Key, a.Value))
        return true
    })
    logMsg := fmt.Sprintf("[%s] %s - %s", r.Level, r.Message, strings.Join(attrs, ", "))
    
    if r.Level >= slog.LevelError && tl.failOnError {
        tl.t.Error(logMsg)
    } else {
        tl.t.Log(logMsg)
    }
    return nil
}

This modification allows you to see structured log data in your test output, which can be crucial for debugging complex scenarios.

hey there, i’ve been using a similar setup and found it pretty helpful. one thing you might wanna consider is adding a way to capture and test for specific log messages. maybe something like:

func (tl *TestLogger) ExpectLog(level slog.Level, msg string) bool {
    // check if log was recorded
}

this could help with more precise testing. just a thought!