9

Disclaimer: I wish you a merry XMas and I hope my question does not disturb you!

sample.go:

package main

import(
    "fmt"
    "os"
)


type sample struct {
    value int64
}

func (s sample) useful() {
    if s.value == 0 {
        fmt.Println("Error: something is wrong!")
        os.Exit(1)
    } else {
        fmt.Println("May the force be with you!")
    }
}

func main() {
    s := sample{42}
    s.useful()

    s.value = 0
    s.useful()
}

// output:
// May the force be with you!
// Error: something is wrong!
// exit status 1

I did a lot of research on how to use interfaces in golang testing. But so far I was not able to wrap my head around this completely. At least I can not see how interfaces help me when I need to "mock" (apologies for using this word) golang std. library packages like "fmt".

I came up with two scenarios:

  1. use os/exec to test the command line interface
  2. wrap fmt package so I have control and am able to check the output strings

I do not like both scenarios:

  1. I experience going through the actual command line a convoluted and not-performant (see below). Might have portability issues, too.
  2. I believe this is the way to go but I fear that wrapping the fmt package might be a lot of work (at least wrapping the time package for testing turned out a non-trivial task (https://github.com/finklabs/ttime)).

Actual Question here: Is there another (better/simpler/idiomatic) way? Note: I want to do this in pure golang, I am not interested in the next testing framework.

cli_test.go:

package main

import(
    "os/exec"
    "testing"
)


func TestCli(t *testing.T) {
    out, err := exec.Command("go run sample.go").Output()
    if err != nil {
        t.Fatal(err)
    }
    if string(out) != "May the force be with you!\nError: this is broken and not useful!\nexit status 1" {
        t.Fatal("There is something wrong with the CLI")
    }
}
moin moin
  • 2,263
  • 5
  • 32
  • 51

2 Answers2

12

Chapter 11 of Kerningham's Book gives a good solution to this question. The trick is to change the calls to fmt.Printline() to calls to fmt.Fprint(out, ...) where out is initialised to os.Stdout

This can be overwritten in the test harness to new(bytes.Buffer) allowing the test to capture the output.

See https://github.com/adonovan/gopl.io/blob/master/ch11/echo/echo.go and https://github.com/adonovan/gopl.io/blob/master/ch11/echo/echo_test.go

edited by OP... sample.go:

package main


import(
    "fmt"
    "os"
    "io"
)


var out io.Writer = os.Stdout // modified during testing
var exit func(code int) = os.Exit

type sample struct {
    value int64
}


func (s sample) useful() {
    if s.value == 0 {
        fmt.Fprint(out, "Error: something is wrong!\n")
        exit(1)
    } else {
        fmt.Fprint(out, "May the force be with you!\n")
    }
}


func main() {
    s := sample{42}
    s.useful()

    s.value = 0
    s.useful()
}

// output:
// May the force be with you!
// Error: this is broken and not useful!
// exit status 1

cli_test.go:

package main

import(
    "bytes"
    "testing"
)


func TestUsefulPositive(t *testing.T) {
    bak := out
    out = new(bytes.Buffer)
    defer func() { out = bak }()

    s := sample{42}
    s.useful()
    if out.(*bytes.Buffer).String() != "May the force be with you!\n" {
        t.Fatal("There is something wrong with the CLI")
    }

}


func TestUsefulNegative(t *testing.T) {
    bak := out
    out = new(bytes.Buffer)
    defer func() { out = bak }()
    code := 0
    osexit := exit
    exit = func(c int) { code = c }
    defer func() { exit = osexit }()

    s := sample{0}
    s.useful()
    if out.(*bytes.Buffer).String() != "Error: something is wrong!\n" {
        t.Fatal("There is something wrong with the CLI")
    }
    if code != 1 {
        t.Fatal("Wrong exit code!")
    }
}
Amnon
  • 334
  • 2
  • 7
  • wow, I did not know that Pike wrote a book. Could you please state the title or a reference to the book. I like your advice. I am about to refactor my code like that and report back. Thank you so much! – moin moin Dec 25 '15 at 14:33
  • 1
    The Go Programming Language http://www.gopl.io/ Alan A. A. Donovan · Brian W. Kernighan Published Oct 26, 2015 in paperback and Nov 20 in e-book Addison-Wesley; 380pp; ISBN: 978-0134190440 – Amnon Dec 25 '15 at 15:00
  • I refactored the code following your advice. I hope that you do not mind that I added what I came up to your answer to make it explicit. If I misunderstood something please change it. By the way there seems to be a problem with the os.Exit. – moin moin Dec 25 '15 at 15:01
  • got it. I updated your answer to reflect that. Thanks for your help! – moin moin Dec 25 '15 at 17:31
  • 1
    That works. But best to avoid calling Exit, and instead have useful return an error (which would be nil in the usual case). See https://golang.org/doc/effective_go.html#errors – Amnon Dec 27 '15 at 06:31
  • you are absolutely right! I hoped I would get away with using exit() calls and I almost did. It bit me in the end and now I am going to read up Chapter 5.4 on error handling strategies. So far I like the book you recommend. Again thank you for the tip. – moin moin Dec 27 '15 at 09:01
2

Am I missing something here or are you talking of testable examples?

Basically, it works like this: In a *_test.go file, you need to adhere to the convention Example[[T][_M]] where T is a placeholder for the type and M a placeholder for the method you want to display the testable example as example code in the Godoc. If the function is just called Example(), the code will be shown as a package example.

Below the last line of the code of your example, you can put a comment like this

// Output:
// Foo

Now go test will make sure that the testable example function either exactly puts out everything below // Output: (including whitespace) or it will make the test fail.

Here is an actual example for an testable example

func ExampleMongoStore_Get() {

  sessionId := "ExampleGetSession"

  data, err := ms.Get(sessionId)

  if err == sessionmw.ErrSessionNotFound {

    fmt.Printf("Session '%s' not found\n", sessionId)

    data = make(map[string]interface{})
    data["foo"] = "bar"

    ms.Save(sessionId, data)
  }

  loaded, _ := ms.Get(sessionId)
  fmt.Printf("Loaded value '%s' for key '%s' in session '%s'",
    loaded["foo"],
    "foo", sessionId)
  // Output:
  // Session 'ExampleGetSession' not found
  // Loaded value 'bar' for key 'foo' in session 'ExampleGetSession'
}

Edit: Have a look at the output of above example at godoc.org

Markus W Mahlberg
  • 19,711
  • 6
  • 65
  • 89