5

I'd like to test the output of a golang command line app, but I'm not quite sure how to do that with go's testing library.

Let's say I have a program like this:

package main

import (
    "flag"
    "fmt"
)

func main() {
    const (
        cityDefault = "San Francisco"
        cityDoc     = "the city you want the forecast for"
    )
    var city string
    flag.StringVar(&city, "city", cityDefault, cityDoc)
    flag.StringVar(&city, "c", cityDefault, cityDoc)
    flag.Parse()

    fmt.Println(city)
}

I'd like to test that both of these:

$ ./myapp -c "Los Angeles"
$ ./myapp -city "Los Angeles"

... output Lost Angeles. So, I guess the question is, how do you go about integration testing the output of a golang command line app?

Elliot Larson
  • 10,669
  • 5
  • 38
  • 57
  • @TheHippo My question isn't really about how to test the flags themselves. The example I've given sort of boils down to that, but mostly because it's a trivial example. Imagine a more complicated app where there are a number of other pieces of functionality that are tested in isolation. I'd like to integration test the output of the app that uses these pieces together. – Elliot Larson Dec 03 '14 at 18:35

2 Answers2

4

This is a bad example of parsing command line args, but it shows the framework I use to test command line args in my apps.

main.go

package main

import (
    "log"
    "os"
)

func main() {
    var city string
    parseFlags(&city, os.Args)

    log.Println(city)
}

func parseFlags(result *string, args []string) {
    cityDefault := "San Francisco"

    switch len(args) {
    case 3:
        *result = args[2]
    default:
        *result = cityDefault
    }
}

main_unit_test.go

package main

import (
    "log"
    "testing"
)

// TestParseFlags - test our command line flags
func TestParseFlags(t *testing.T) {
    var parseFlagsTests = []struct {
        flags    []string // input flags to the command line
        expected string   // expected
    }{
        {[]string{"/fake/loc/main"}, "San Francisco"},
        {[]string{"/fake/loc/main", "-c", "Los Angeles"}, "Los Angeles"},
        {[]string{"/fake/loc/main", "--city", "Los Angeles"}, "Los Angeles"},
    }

    for _, tt := range parseFlagsTests {
        var output string
        parseFlags(&output, tt.flags)
        if output != tt.expected {
            t.Errorf("expected: %s, actual: %s", tt.expected, output)
        }
    }
}

I typically use this package to parse command line args in all of my apps. And I'll structure my code as follows (tests not shown, but they generally follow the gist of the test shown above):

main.go

package main

import (
    "log"
    "os"

    "myDir/cli"
)

func main() {
    // Grab the user inputed CLI flags
    cliFlags := cli.FlagsStruct{}
    cliErr := cli.StartCLI(&cliFlags, os.Args)
    if cliErr != nil {
        log.Println("Error grabbing command line args")
        log.Fatal(cliErr)
    }

    // Do stuff ...
}

/myDir/cli.go

package cli

import "github.com/urfave/cli"

// FlagsStruct - holds command line args
type FlagsStruct struct {
    MyFlag string
}

// StartCLI - gathers command line args
func StartCLI(cliFlags *FlagsStruct, args []string) error {
    app := cli.NewApp()
    app.Action = func(ctx *cli.Context) error {
        MyFlag := ctx.GlobalString("my-flag")

        // build the cli struct to send back to main
        cliFlags.MyFlag = MyFlag
        return nil
    }
    app.Authors = []cli.Author{
        {
            Email: "my@email.com",
            Name:  "Adam Hanna",
        },
    }
    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:  "my-flag, f",
            Usage: "My flag usage goes here",
            Value: "myDefault",
        },
    }
    app.Name = "myAppName"
    app.Usage = "My App's Usage"
    app.Version = "0.0.1"
    return app.Run(args)
}
Adam
  • 3,142
  • 4
  • 29
  • 48
2

How about test "$(./myapp -c "Los Angeles")" = "Los Angeles" and the same for -city. This has nothing to do with Go, just let your integration test suite do the test.

Volker
  • 40,468
  • 7
  • 81
  • 87
  • Sure, that makes sense. I guess I was hoping for a little more guidance from you about how you go about doing this. But I guess that ends up getting a little too open ended for a Stack Overflow response, as it would devolve into an opinionated toolchain discussion (heaven forbid). However, I think this points me in the right direction. Thanks. – Elliot Larson Dec 03 '14 at 18:47