How to test a function's output (stdout/stderr) in unit tests
Asked Answered
D

4

36

I have a simple function I want to test:

func (t *Thing) print(min_verbosity int, message string) {
    if t.verbosity >= minv {
        fmt.Print(message)
    }
}

But how can I test what the function actually sends to standard output? Test::Output does what I want in Perl. I know I could write all my own boilerplate to do the same in Go (as described here):

orig = os.Stdout
r,w,_ = os.Pipe()
thing.print("Some message")
var buf bytes.Buffer
io.Copy(&buf, r)
w.Close()
os.Stdout = orig
if(buf.String() != "Some message") {
    t.Error("Failure!")
}

But that's a lot of extra work for every single test. I'm hoping there's a more standard way, or perhaps an abstraction library to handle this.

Delaminate answered 7/11, 2014 at 15:33 Comment(0)
F
53

One thing to also remember, there's nothing stopping you from writing functions to avoid the boilerplate.

For example I have a command line app that uses log and I wrote this function:

func captureOutput(f func()) string {
    var buf bytes.Buffer
    log.SetOutput(&buf)
    f()
    log.SetOutput(os.Stderr)
    return buf.String()
}

Then used it like this:

output := captureOutput(func() {
    client.RemoveCertificate("www.example.com")
})
assert.Equal(t, "removed certificate www.example.com\n", output)

Using this assert library: http://godoc.org/github.com/stretchr/testify/assert.

Floeter answered 7/11, 2014 at 16:50 Comment(4)
This is probably what I will do, as it's a more general solution than that provided by @Ainar-G, which requires that I control all of the code which might output something.Delaminate
FYI this is not go-routine safe. A second go-routine could reset log back to os.Stderr while the first go-routing is still logging.Highly
FYI: This will only capture log outputs and not fmt outputs.Officeholder
For testing concurrent actions that have log output, this can end up in a race between writing to the buffer and reading from it. For a solution, see my answer here: https://mcmap.net/q/427461/-how-to-check-a-log-output-in-go-testLiterature
K
21

You can do one of three things. The first is to use Examples.

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
}

The second (and more appropriate, IMO) is to use fake functions for your IO. In your code you do:

var myPrint = fmt.Print

func (t *Thing) print(min_verbosity int, message string) {
    if t.verbosity >= minv {
        myPrint(message) // N.B.
    }
}

And in your tests:

func init() {
    myPrint = fakePrint // fakePrint records everything it's supposed to print.
}

func Test...

The third is to use fmt.Fprintf with an io.Writer that is os.Stdout in production code, but bytes.Buffer in tests.

Koerner answered 7/11, 2014 at 15:48 Comment(4)
Examples are good for when you have deterministic output and fmt.Fprint is good for when you can control the print code (which you can't if it's in an external library or something). I think most of the time one of these things will be true. If there's a case where neither is true an option to read what is written to os.Stdout into a string would be nice.Aliped
I actually used a combination of option 2 and 3. First I made a printer interface, which is used throughout my codebase. This makes testing my code easy and gives me full control over what goes to the screen. Second I test my printer, which can print various complicated types of output to stdout and stderr, using option 3. For unit testing my printer I simply give it two bytes.Buffer writers instead of os.Stdout and os.StdErr.Sturges
How do you do number 3?Reproductive
@QuolonelQuestions - I believe the answer is, change all your production code prints to use an injected dependency. For example, instead of fmt.Println(x) always use fmt.Println(MyWriter, x) where MyWriter is a global that defaults to io.Writer. That might be a lot of change to production code.Guillerminaguillermo
G
3

Adapting the answer from @Caleb to testing and stdout, with some fixes from the OP's code:

func captureOutput(f func() error) (string, error) {
    orig := os.Stdout
    r, w, _ := os.Pipe()
    os.Stdout = w
    err := f()
    os.Stdout = orig
    w.Close()
    out, _ := io.ReadAll(r)
    return string(out), err
}

func TestMyFunc(t *testing.T) {
    output, err := captureOutput(func() error {
        err := myFunc("arg1", "arg2")
        return err
    })
    assert.Nil(t, err)
    assert.Equal(t, "foo", output)
}

Note the location of Close(). Before I moved this above the ReadAll(), the read would hang and cause a test error, for the obvious reason (waiting for more input.)

Guillerminaguillermo answered 21/9, 2023 at 16:14 Comment(0)
F
1

You could consider adding a return statement to your function to return the string that is actually printed out.

func (t *Thing) print(min_verbosity int, message string) string {
    if t.verbosity >= minv {
        fmt.Print(message)
        return message
    }
    return ""
}

Now, your test could just check the returned string against an expected string (rather than the print out). Maybe a bit more in-line with Test Driven Development (TDD).

And, in your production code, nothing would need to change, since you don't have to assign the return value of a function if you don't need it.

Functionary answered 7/11, 2014 at 18:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.