Search code examples
unit-testinggogo-cobra

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.


Solution

  • 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.