How to fake os.Stdout in golang for testing?

3,517 views
Skip to first unread message

mfried...@gmail.com

unread,
May 6, 2018, 12:35:08 AM5/6/18
to golang-nuts
Hello,

I am trying out golang and like it's concepts (coming from Python and Java mostly, not having Exceptions felt a bit strange at the start, though :-)).

* Now I try to write tests for https://github.com/caradojo/trivia/blob/master/go/trivia.go for the fun of it.
* I want to do this without modifying the code (too much).
* I already introduced my own fake[1] random and now need to replace os.Stdout with a fake implementation as the results of the game implemented here are written with `fmt.Println` resp. `fmt.Printf`.
* My first approach was to use a tempfile[2], this works but I do not like to touch the disk during tests.
* In Java `System.out` is just a `PrintStream` which is easily replaced by sth. like `System.setOut(new PrintStream(new ByteArrayOutputStream()));`
* Now in golang `os.Stdout` is an `*os.File` which is mostly a wrapper for an OS specific file.

I now tried something like this:
```
type file interface {
io.Writer
}

type fakeFile struct {
out *bytes.Buffer
}

func (ff fakeFile) Write(p []byte) (n int, err error) {
return ff.out.Write(p)
}

func foo()
realStdout := os.Stdout
defer func() { os.Stdout = realStdout }()
var out []byte
var f file = fakeFile{bytes.NewBuffer(out)}
os.Stdout = &os.File{&f}
)
```

This does not compile but I get "cannot use &f (type *file) as type *os.file in field value".

Then I tried a Pythonesque approach, something like this:
```
realStdoutWrite := os.Stdout.Write
defer func() { os.Stdout.Write = realStdoutWrite }()
var outb = bytes.NewBufferString("")
os.Stdout.Write = outb.Write
```
but get "cannot assign to os.Stdout.Write"

In a real world example I would not write to os.Stdout but inject an `io.Writer` which would be used,
so this is more a question whether I miss something or is faking os.Stdout really hard?

Any hints appreciated :-)

Regards
Mirko

[1] https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs
[2] https://github.com/mfriedenhagen/trivia/commit/b8ecec0097bfb4a6e7e2992e1401274cff06c03b#diff-2a08ba5d9ea423f34350a051d2333274R44

Lars Seipel

unread,
May 6, 2018, 1:32:47 AM5/6/18
to mfried...@gmail.com, golang-nuts
On Sat, May 05, 2018 at 08:55:17AM -0700, mfried...@gmail.com wrote:
> * Now in golang `os.Stdout` is an `*os.File` which is mostly a wrapper for
> an OS specific file.

You can always use os.Pipe to get an actual file descriptor to write to.

alex....@gmail.com

unread,
May 6, 2018, 2:21:14 AM5/6/18
to golang-nuts
Or use log instead of fmt

var stdout = log.New(os.Stdout, "", 0)

Then you can easily redirect it to any io.Writter like

buf := &strings.Builder{}
stdout
= log.New(buf, "", 0)

mfried...@gmail.com

unread,
May 6, 2018, 6:26:37 AM5/6/18
to golang-nuts

Hello Lars,

thanks, I now have

func dieOn(err error, t *testing.T) {
if err != nil {
t.Fatal(err)
}
}

// Returns output to `os.Stdout` from `runnable` as string.
func catchStdOut(t *testing.T, runnable func()) (string) {

realStdout := os.Stdout
defer func() { os.Stdout = realStdout }()
   r, fakeStdout, err := os.Pipe()
dieOn(err, t)
os.Stdout = fakeStdout
runnable()
// need to close here, otherwise ReadAll never gets "EOF".
dieOn(fakeStdout.Close(), t)
newOutBytes, err := ioutil.ReadAll(r)
dieOn(err, t)
dieOn(r.Close(), t)
return string(newOutBytes)
}

I like that better
 

mfried...@gmail.com

unread,
May 6, 2018, 6:44:10 AM5/6/18
to golang-nuts

On Sunday, May 6, 2018 at 8:21:14 AM UTC+2, alex....@gmail.com wrote:
Or use log instead of fmt

var stdout = log.New(os.Stdout, "", 0)

Then you can easily redirect it to any io.Writter like

buf := &strings.Builder{}
stdout
= log.New(buf, "", 0)

Hello Alex,

good point, however firstly I do not want to modify the original code. My first goal is to cover it completely with tests. This being a test project for legacy code refactoring, the program *must* always produce the output given here: https://github.com/mfriedenhagen/trivia/blob/master/reference/result.txt.

Regards
Mirko

alex....@gmail.com

unread,
May 6, 2018, 7:44:11 AM5/6/18
to golang-nuts
The output of text printed with the log as created in my example and fmt are exactly the same. 
The only change you would have to make is have a global log var and then do a simple search and replace all fmt.Printf to logVar.Printf.

So the only argument you gave for not changing would be not wanting to change the original source.
If changing the code is an absolute impossibility then sure the os.Stdout redirect makes sense.

Manlio Perillo

unread,
May 6, 2018, 8:57:41 AM5/6/18
to golang-nuts
Il giorno domenica 6 maggio 2018 06:35:08 UTC+2, Mirko Friedenhagen ha scritto:
Hello,

I am trying out golang and like it's concepts (coming from Python and Java mostly, not having Exceptions felt a bit strange at the start, though :-)).

* Now I try to write tests for https://github.com/caradojo/trivia/blob/master/go/trivia.go for the fun of it.
* I want to do this without modifying the code (too much).
* I already introduced my own fake[1] random and now need to replace os.Stdout with a fake implementation as the results of the game implemented here are written with `fmt.Println` resp. `fmt.Printf`.
* My first approach was to use a tempfile[2], this works but I do not like to touch the disk during tests.
* In Java `System.out` is just a `PrintStream` which is easily replaced by sth. like `System.setOut(new PrintStream(new ByteArrayOutputStream()));`
* Now in golang `os.Stdout` is an `*os.File` which is mostly a wrapper for an OS specific file.

For your example code the solution is simple: don't use fmt.Printf, but use fmt.Fprintf that accepts an io.Writer instead.  Pass the writer as the function argument.

You can also use the log package, but I personally use it *only* to report (handled) errors to the terminal or to the log file.

> [...]

Manlio 
Reply all
Reply to author
Forward
0 new messages