Testing Go CLI Apps

Post by Saul Shanabrook

I recently started using the really great go.cli library to develop a super simple command line server in Go. When testing this server, however, I needed to figure out a way of starting and stopping it on demand. It is very easy to start a CLI app, just call the Run method with a list of arguments.

To stop it, I realized I could use golang.org/x/net/context library, to pass a context into the app and then cancel it after I wanted the test to stop.

First I had to figure out a way create an app dynamically with a context.Context. For some reason, it took me while to figure out I could just wrap making the cli app in a function, that took a context.Context and executed the action using this.

package main

import (  
    "os"
    "strconv"

    "golang.org/x/net/context"

    "github.com/codegangsta/cli"

    "github.com/lucibus/subicul/websocketserver"
)


func makeCliApp(ctx context.Context) *cli.App {  
    app := cli.NewApp()
    app.Name = "subicul"
    app.Usage = "lighting server"

    app.Flags = []cli.Flag{
        cli.IntFlag{
            Name:   "port",
            Value:  8080,
            Usage:  "TCP port to listen on",
            EnvVar: "SUBICUL_PORT",
        },
    }

    app.Action = func(c *cli.Context) {
        err := websocketserver.MakeStateServer(ctx, c.Int("port"))
        if err != nil {
            log.Fatalln(err)
        }
        // wait here until the context is cancelled
        <-ctx.Done()
    }
    return app
}

func main() {  
    app := makeCliApp(context.Background())
    app.Run(os.Args)
}

Then to test it, I could pass in a custom context and cancel it later, like this (using GoConvey syntax):

package main

import (  
    "fmt"
    "net/http"
    "strconv"
    "testing"
    "time"

    "github.com/gorilla/websocket"
    "golang.org/x/net/context"

    . "github.com/smartystreets/goconvey/convey"
)

// TestMainCLI tests that the server starts at a certain
// port and accepts websockets connections on that port.
// It also verifies that the server is stopped after
func TestMainCLI(t *testing.T) {  
    Convey("when I run the app", t, func() {
        port := 9001

        ctx, cancelFunc := context.WithCancel(context.Background())

        app := makeCliApp(ctx)
        go app.Run([]string{"<executable path>", "--port", strconv.Itoa(port)})
        time.Sleep(time.Millisecond)

        url := fmt.Sprintf("ws://localhost:%v/", port)
        d := websocket.Dialer{}
        Convey("and connect", func() {
            conn, _, err := d.Dial(url, http.Header{})
            So(err, ShouldBeNil)
            conn.Close()
        })

        Reset(func() {
            cancelFunc()
            time.Sleep(time.Millisecond)
            So("subicul", ShouldNotBeRunningGoroutines)
        })

    })

}

You might also notice the use of the ShouldNotBeRunningGoroutines function, which I just blogged about

I am new to Go, so let me know if I am doing anything stupid.

This post was inspired by Peter Bourgon's great talk, which asked the Go community to talk more about their approaches to using the context package.