How to implement unit tests for CLI commands in go
Asked Answered
M

2

9

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.

Moorer answered 13/1, 2020 at 0:29 Comment(1)
Why do you use init function and global variables there at all?Subsidiary
A
5

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.

Angst answered 13/1, 2020 at 9:51 Comment(2)
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/10Closefisted
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 :(Sisyphean
L
2

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.

Louden answered 13/1, 2020 at 7:36 Comment(1)
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.Moorer

© 2022 - 2025 — McMap. All rights reserved.