A lightweight test helper package
0

Configure Feed

Select the types of activity you want to include in your feed.

Add a CaptureOutput function to trap stdout and stderr (#18)

* Add a CaptureOutput function to trap stdout and stderr

* Flesh out the docstring

* Describe error case in function doc comment

authored by

Tom Fleet and committed by
GitHub
(Jan 18, 2024, 2:35 PM UTC) 933aa62a e2333f66

+128 -1
-1
.golangci.yml
··· 23 23 - nilerr 24 24 - nilnil 25 25 - nolintlint 26 - - nonamedreturns 27 26 - predeclared 28 27 - reassign 29 28 - revive
+92
test.go
··· 5 5 import ( 6 6 "bytes" 7 7 "fmt" 8 + "io" 8 9 "math" 9 10 "os" 10 11 "path/filepath" 11 12 "reflect" 13 + "sync" 12 14 "testing" 13 15 14 16 "github.com/google/go-cmp/cmp" ··· 212 214 contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n")) 213 215 214 216 Diff(t, string(contents), want) 217 + } 218 + 219 + // CaptureOutput captures and returns data printed to stdout and stderr by the provided function fn, allowing 220 + // you to test functions that write to those streams and do not have an option to pass in an [io.Writer]. 221 + // 222 + // If the provided function returns a non nil error, the test is failed with the error logged as the reason. 223 + // 224 + // If any error occurs capturing stdout or stderr, the test will also be failed with a descriptive log. 225 + // 226 + // fn := func() error { 227 + // fmt.Println("hello stdout") 228 + // return nil 229 + // } 230 + // 231 + // stdout, stderr := test.CaptureOutput(t, fn) 232 + // fmt.Print(stdout) // "hello stdout\n" 233 + // fmt.Print(stderr) // "" 234 + func CaptureOutput(t testing.TB, fn func() error) (stdout, stderr string) { 235 + t.Helper() 236 + 237 + // Take copies of the original streams 238 + oldStdout := os.Stdout 239 + oldStderr := os.Stderr 240 + 241 + defer func() { 242 + // Restore everything back to normal 243 + os.Stdout = oldStdout 244 + os.Stderr = oldStderr 245 + }() 246 + 247 + stdoutReader, stdoutWriter, err := os.Pipe() 248 + if err != nil { 249 + t.Fatalf("CaptureOutput: could not construct an os.Pipe(): %v", err) 250 + } 251 + 252 + stderrReader, stderrWriter, err := os.Pipe() 253 + if err != nil { 254 + t.Fatalf("CaptureOutput: could not construct an os.Pipe(): %v", err) 255 + } 256 + 257 + // Set stdout and stderr streams to the pipe writers 258 + os.Stdout = stdoutWriter 259 + os.Stderr = stderrWriter 260 + 261 + stdoutCapture := make(chan string) 262 + stderrCapture := make(chan string) 263 + 264 + var wg sync.WaitGroup 265 + wg.Add(2) //nolint: gomnd 266 + 267 + // Copy in goroutines to avoid blocking 268 + go func(wg *sync.WaitGroup) { 269 + defer func() { 270 + close(stdoutCapture) 271 + wg.Done() 272 + }() 273 + buf := &bytes.Buffer{} 274 + if _, err := io.Copy(buf, stdoutReader); err != nil { 275 + t.Fatalf("CaptureOutput: failed to copy from stdout reader: %v", err) 276 + } 277 + stdoutCapture <- buf.String() 278 + }(&wg) 279 + 280 + go func(wg *sync.WaitGroup) { 281 + defer func() { 282 + close(stderrCapture) 283 + wg.Done() 284 + }() 285 + buf := &bytes.Buffer{} 286 + if _, err := io.Copy(buf, stderrReader); err != nil { 287 + t.Fatalf("CaptureOutput: failed to copy from stderr reader: %v", err) 288 + } 289 + stderrCapture <- buf.String() 290 + }(&wg) 291 + 292 + // Call the test function that produces the output 293 + if err := fn(); err != nil { 294 + t.Fatalf("CaptureOutput: user function returned an error: %v", err) 295 + } 296 + 297 + // Close the writers 298 + stdoutWriter.Close() 299 + stderrWriter.Close() 300 + 301 + capturedStdout := <-stdoutCapture 302 + capturedStderr := <-stderrCapture 303 + 304 + wg.Wait() 305 + 306 + return capturedStdout, capturedStderr 215 307 }
+36
test_test.go
··· 191 191 } 192 192 } 193 193 194 + func TestCapture(t *testing.T) { 195 + t.Run("happy", func(t *testing.T) { 196 + // Some fake user function that writes to stdout and stderr 197 + fn := func() error { 198 + fmt.Fprintln(os.Stdout, "hello stdout") 199 + fmt.Fprintln(os.Stderr, "hello stderr") 200 + 201 + return nil 202 + } 203 + 204 + stdout, stderr := test.CaptureOutput(t, fn) 205 + 206 + test.Equal(t, stdout, "hello stdout\n") 207 + test.Equal(t, stderr, "hello stderr\n") 208 + }) 209 + 210 + t.Run("sad", func(t *testing.T) { 211 + // This time the user function returns an error 212 + fn := func() error { 213 + return errors.New("it broke") 214 + } 215 + 216 + buf := &bytes.Buffer{} 217 + testTB := &TB{out: buf} 218 + 219 + stdout, stderr := test.CaptureOutput(testTB, fn) 220 + 221 + // Test should have failed 222 + test.True(t, testTB.failed) 223 + 224 + // stdout and stderr should be empty 225 + test.Equal(t, stdout, "") 226 + test.Equal(t, stderr, "") 227 + }) 228 + } 229 + 194 230 // Always returns a nil error, needed because manually constructing 195 231 // nil means it's not an error type but here it is. 196 232 func nilErr() error {