Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement unit tests for CLI commands in go

I'm starting a new OSS CLI tool that utilizes spf13/cobra. Being new to golang, I'm having a hard time figuring out the best way to test commands in isolation. Can anybody give me an example of how to test a command? Couple of caveats:

  1. you can't return a cobra.Command from your init function
  2. you can't have get_test.go in the cmd directory...which I was under the impression was the golang best practice.
  3. I'm new to golang, go easy on me :sweat_smile:

Please correct me if I'm wrong.

Here's the cmd I'm trying to test: https://github.com/sahellebusch/raider/blob/3-add-get-alerts/cmd/get.go.

Open to ideas, suggestions, criticisms, whatever.

like image 929
Busch Avatar asked Jan 13 '20 00:01

Busch


People also ask

Is go good for CLI apps?

Go is a particularly good language for CLI's I have found. At least compared to Java/C#/Python. It's reasonably fast, compiles down to a simple to distribute binary, and the language is forgiving enough that you can do exploratory programming in it.

How do I test my go code?

At the command line in the greetings directory, run the go test command to execute the test. The go test command executes test functions (whose names begin with Test ) in test files (whose names end with _test.go). You can add the -v flag to get verbose output that lists all of the tests and their results.


2 Answers

There are multiple approaches to implementing a CLI using go. This is the basic structure of the CLI I developed which is mostly influenced by the docker CLI and I have added unit tests as well.

The first thing you need is to have CLI as an interface. This will be inside a package named "cli".

package cli

type Cli interface {
     // Have interface functions here
     sayHello() error
}

This will be implemented by 2 clis: HelloCli (Our real CLI) and MockCli (used for unit tests)

package cli

type HelloCli struct {
}

func NewHelloCli() *HelloCli {
    cli := &HelloCli{
    }
    return cli
}

Here the HelloCli will implement sayHello function as follows.

package cli

func (cli *HelloCli) SayHello() error {
    // Implement here
}

Similarly, there will be a mock cli in a package named test that would implement cli interface and it will also implement the sayHello function.

package test

type MockCli struct {
    }

    func NewMockCli() *HelloCli {
        cli := &MockCli{
        }
        return cli
    }

func (cli *MockCli) SayHello() error {
        // Mock implementation here
    }

Now I will show how the command is added. First I would have the main package and this is where I would add all the new commands.

package main

func newCliCommand(cli cli.Cli) *cobra.Command {
    cmd := &cobra.Command{
        Use:   "foo <command>"
    }

    cmd.AddCommand(
        newHelloCommand(cli),
    )
    return cmd
}

func main() {
    helloCli := cli.NewHelloCli()
    cmd := newCliCommand(helloCli)
    if err := cmd.Execute(); err != nil {
        // Do something here if execution fails
    }
}

func newHelloCommand(cli cli.Cli) *cobra.Command {
    cmd := &cobra.Command{
        Use:   "hello",
        Short: "Prints hello",
        Run: func(cmd *cobra.Command, args []string) {
            if err := pkg.RunHello(cli, args[0]); err != nil {
                // Do something if command fails
            }
        },
        Example: "  foo hello",
    }
    return cmd
}

Here, I have one command called hello. Next, I will have the implementation in a separate package called "pkg".

package pkg

func RunHello(cli cli.Cli) error {
    // Do something in this function
    cli.SayHello()
    return nil
}

The unit tests will also be contained in this package in a file named hello_test.

package pkg

func TestRunHello(t *testing.T) {
    mockCli := test.NewMockCli()

    tests := []struct {
        name     string
    }{
        {
            name:     "my test 1",
        },
        {
            name:     "my test 2"
        },
    }
    for _, tst := range tests {
        t.Run(tst.name, func(t *testing.T) {
            err := SayHello(mockCli)
            if err != nil {
                t.Errorf("error in SayHello, %v", err)
            }
        })
    }
}

When you execute foo hello, the HelloCli will be passed to the sayHello() function and when you run unit tests, MockCli will be passed.

like image 140
Madhuka Wickramapala Avatar answered Nov 14 '22 21:11

Madhuka Wickramapala


You can check how cobra itself does it - https://github.com/spf13/cobra/blob/master/command_test.go

Basically you can refactor the actual Command logic(the run function) into a separate function and test that function. You probably want to name your functions properly instead of just calling it run.

like image 42
Shashank V Avatar answered Nov 14 '22 22:11

Shashank V