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.
