Mat Ryer Profile picture
Building things at @grafana • Indoor enthusiast • Comedy • Music • Podcaster • Author • Opensourcer

Sep 15, 2020, 6 tweets

I always call into a run function from main in #golang passing in any system dependencies as arguments.

If I am going to parse flags, use arguments, and write to stdout, my run func looks like this:

func run(args []string, stdout io.Writer) error

pace.dev/blog/2020/02/1…

1/6

This means I can write normal Go tests that can call the run function, and there's no global state to mess around with.

func Test(t *testing.T) {
args := []string{"tool", "--debug=true", "./testdata"}
var stdout bytes.Buffer
err := run(args, stdout)
//...

2/6

This is nice because I can use any `io.Writer` as the `stdout`. In my tests, I use bytes.Buffer - which allows me to capture (and make assertions about) what the tool outputs.

I can also play around with any flags or arguments without having to do anything weird :)

3/6

You can pass in anything that you might otherwise go straight to the os package for.

os.Args, os.Stdin, os.Stdout, os.Stderr

You can even use a `func(string) string` to stand in for os.Getenv - then you can control the env vars in test code.

4/6

Returning an error from run is also a nice benefit. Since you cannot return error from `main` functions in Go, you can be left writing error handling code multiple times.

With our run abstraction, we just have to handle the returned error once.

5/6

So `main` ends up being untested, but since it's just calling `run` and passing in things from the os package, we can trust ourselves to get that right.

func main() {
err := run(os.Args, os.Stdin, os.Stdout, os.Stderr)
if err != nil {
// handle program errors
}
}

6/6

Share this Scrolly Tale with your friends.

A Scrolly Tale is a new way to read Twitter threads with a more visually immersive experience.
Discover more beautiful Scrolly Tales like this.

Keep scrolling