Unit testing slog output

1,218 views
Skip to first unread message

shan...@gmail.com

unread,
Jun 13, 2023, 8:14:27 PM6/13/23
to golang-nuts
In the past when I wanted to 'capture' the log output so that I could check it in a unit test I would 'hijack' os.Stdout like so
```
var osStdout = os.Stdout
func MyCode (){
  log.SetOutput(osStdout)
  log.Print("My dog has fleas")
}
```
Which could then be tested thus
```
func TestMyCode(t *testing.T){
  testcases := map[string]struct{
  output string
  }{
    "Dog needs flea shampoo": {
      output: "My dog has fleas",
    },
  }
  for name, tc := range testcases {
      t.Run(name, func(t *testing.T) {
         orig := osStdout
         flags := log.Flags()
         log.SetFlags(0)
         reader, writer, err := os.Pipe()
         if err != nil {
            panic(err)
         }
         osStdout = writer
         defer func() {
           osStdout = orig
           log.SetFlags(flags)
          }()
         MyCode()
         writer.Close()

         var buf strings.Builder
         if _, ioerr := io.Copy(&buf, reader); ioerr != nil {
           log.Fatalf("Copy error, cannot continue %v\n", ioerr)
         }

          assert.Equal(t, tc.output, buf.String(), "Expected: %s Got: %s", tc.output, buf.String())
         }
       })
    }
}
```

I'm now trying to do the same with the slog package (but not having any joy) - is there an established pattern that people are using, eg. is the handler being

I've been trying
```
func TestMyCode(t *testing.T){
  testcases := map[string]struct{
  output string
  }{
    "Dog needs flea shampoo": {
      output: "My dog has fleas",
    },
  }
  for name, tc := range testcases {
      t.Run(name, func(t *testing.T) {
         orig := osStdout
         reader, writer, err := os.Pipe()
         if err != nil {
            panic(err)
         }
         osStdout = writer
         defer func() {
           osStdout = orig
          }()
         slog.SetDefault(slog.New(slog.NewTextHandler(osStdout)
         MyCode()
         writer.Close()

         var buf strings.Builder
         if _, ioerr := io.Copy(&buf, reader); ioerr != nil {
           log.Fatalf("Copy error, cannot continue %v\n", ioerr)
         }

          assert.Equal(t, tc.output, buf.String(), "Expected: %s Got: %s", tc.output, buf.String())
         }
       })
    }
}
```

Which "works" but I am not happy with it, because it's affecting all of the slog instances.

I saw previously that someone was creating their own implementation of a slog.Handler? to make testing easier, and would love to see it, and will that allow me to run multiple slog based tests concurrently?

Tamás Gulácsi

unread,
Jun 14, 2023, 12:44:39 PM6/14/23
to golang-nuts
github.com/UNO-SOFT/zlog/v2
NewT(t).SLog()
returns an *slog.Logger that uses t.Log for printing.

But maybe I don't understand your real problem.

shan...@gmail.com

unread,
Jun 14, 2023, 7:38:24 PM6/14/23
to golang-nuts
I think that perhaps a bit more explanation might be helpful

I have an established pattern of capturing log messages so that i can check them in unit tests. (The code/test first example)

I want to do that with slog (where slog emits a message and I capture that message and compare it in the test), as partially demonstrated in the final test section..

I've also discovered that i want to drop parts of the message (eg. the timestamp).


The goal is to be able to trigger the SUT to emit a log message that can be tested as both existing, and matching what is expected.
Reply all
Reply to author
Forward
0 new messages