10

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.

1
  • Why do you use init function and global variables there at all? Commented Jan 13, 2020 at 0:35

2 Answers 2

6

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Implemented similar structure for my CLI cloner... Here's the PR if anyone needs to see the simple refactoring required for it github.com/marcellodesales/cloner/pull/10
Can someone have a complete example of this answer ? I am super confused about this topic right now. Especially since the naming in this answer is so similar :(
3

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.

1 Comment

I have actually been thinking about doing this. I don't want to test cobra itself, but I felt like I was leaving out some functionality. But, perhaps this could work.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.