2
\$\begingroup\$

Are there any major flaws to implement this test helper function?

I know that the preferred way (refactoring) would simply make the function to be tested return its value and delegate printing to another part, but sometimes one has to test what is there.

import (
    "io"
    "os"
)

// captureStdout calls a function f and returns its stdout side-effect as string
func captureStdout(f func()) string {
    // return to original state afterwards
    // note: defer evaluates (and saves) function ARGUMENT values at definition
    // time, so the original value of os.Stdout is preserved before it is changed
    // further into this function.
    defer func(orig *os.File) {
        os.Stdout = orig
    }(os.Stdout)

    r, w, _ := os.Pipe()
    os.Stdout = w
    f()
    w.Close()
    out, _ := io.ReadAll(r)

    return string(out)
}

Motivational full example

main.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

main_test.go

package main

import (
    "io"
    "os"
    "testing"
)

func Test_main(t *testing.T) {
    want := "Hello, World!\n"
    got := captureStdout(main)
    if got != want {
        t.Errorf("main() = %v, want %v", got, want)
    }
}

// captureStdout calls a function f and returns its stdout side-effect as string
func captureStdout(f func()) string {
    // return to original state afterwards
    // note: defer evaluates (and saves) function ARGUMENT values at definition
    // time, so the original value of os.Stdout is preserved before it is
    // changed further into this function.
    defer func(orig *os.File) {
        os.Stdout = orig
    }(os.Stdout)

    r, w, _ := os.Pipe()
    os.Stdout = w
    f()
    w.Close()
    out, _ := io.ReadAll(r)

    return string(out)
}
\$\endgroup\$
1
  • \$\begingroup\$ There is a question on Stack Overflow addressing this problem, with some good solutions to reference. \$\endgroup\$ Commented Jan 13, 2023 at 9:19

2 Answers 2

2
\$\begingroup\$

The Go testing package and tools have built-in facilities for this:

From the documentation of testing:

The package also runs and verifies example code. Example functions may include a concluding line comment that begins with "Output:" and is compared with the standard output of the function when the tests are run. (The comparison ignores leading and trailing space.) These are examples of an example:

func ExampleHello() {
    fmt.Println("hello")
    // Output: hello
}

This works suitably for testing output against a static, known result of printable text, like in your example.

\$\endgroup\$
1
  • \$\begingroup\$ Oh wow, this is even better than any review feedback I could have gotten! testing/run_example.go contains a standard library maintained version of my needs. Just the output specification as a comment is a bit quirky, but very fine for my testing use case. \$\endgroup\$
    – ojdo
    Commented Jan 16, 2023 at 9:48
1
\$\begingroup\$
    r, w, _ := os.Pipe()

Why are we ignoring the error return? Do we really believe there's no way for Pipe to fail? Even after a bunch of open()'s with no close()?


    out, _ := io.ReadAll(r)

This is even more puzzling. Read errors can never happen?


Sorry, I cannot recommend shipping this code in its current state.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.