Testing a Cobra CLI in Go

Go has a fantastic library for writing CLI’s (Command Line Interfaces) called Cobra. I’ve been working on a CLI named Deckard for a few months now. Being new to Go, I had (lazily) shied away from writing tests. However, after thinking about my test plan and doing a little refactoring, I’ve found a great way to handle testing your Cobra CLI application.

The idea behind Cobra is that you simply write “Command” functions. These command functions are then called by the Cobra library when it parses a valid command. This means that Cobra handles a lot of the heavy lifting here, and because of that, has a pretty opinionated project structure. Thankfully, Cobra also has a CLI that makes starting a new cobra project a breeze.

Our Cobra Command

Here’s an example of an extremely simple Cobra command from Deckard (commented for clarity):

package cmd

// Import cobra and other dependencies
import (
	"fmt"
	"github.com/spf13/cobra"
)

// caneCmd represents our command
var caneCmd = &cobra.Command{
	Use:   "cane",
	Short: "Ponder mysteries of the Horadrim", // short description
	Long:  `Ponder mysteries of the Horadrim`, // long description
	Run: func(cmd *cobra.Command, args []string) { // the function we want to run
                // Any code here is difficult to test!
		fmt.Println("Stay a while and listen...")
	},
}

// Our init function adds this command as a subcommand of our root command
// which is generated by the Cobra CLI when starting a new project).
func init() {
	rootCmd.AddCommand(caneCmd)
}

Testing our Cobra CLI Command

You’ll notice that we don’t exactly control an entry point into our code. This can make testing rather difficult, since Cobra is responsible for calling our command. However, with a small abstraction, we can make testing our command even easier. There are two similar paths to take with this refactor.

  1. Create a local function and have the anonymous cobra function call the local function.
  2. Create a local function and use a function reference in place of the anonymous function.

The first example can be accomplished like so.

package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
)

func doSomething(cmd *cobra.Command, args []string) {
	fmt.Println("Stay a while and listen...")
}

// caneCmd represents the cane command
var caneCmd = &cobra.Command{
	Use:   "cane",
	Short: "Ponder mysteries of the Horadrim",
	Long:  `Ponder mysteries of the Horadrim`,
	Run: func(cmd *cobra.Command, args []string) {
		doSomething(cmd, args)
	},
}

func init() {
	rootCmd.AddCommand(caneCmd)
}

With this pattern, we can easily test the doSomething function and since the command function simply calls that function, that gives us pretty great coverage. The other pattern, however, is even cleaner.

package cmd

import (
	"fmt"
	"github.com/spf13/cobra"
)

func doSomething(cmd *cobra.Command, args []string) {
	fmt.Println("Stay a while and listen...")
}

// caneCmd represents the cane command
var caneCmd = &cobra.Command{
	Use:   "cane",
	Short: "Ponder mysteries of the Horadrim",
	Long:  `Ponder mysteries of the Horadrim`,
	Run: doSomething,
}

func init() {
	rootCmd.AddCommand(caneCmd)
}

With this pattern, we simply use the function reference instead of a pass-through function. We can still write tests for doSomething, but this code might look a bit more foreign to developers who haven’t seen it before. A trade-off for everything, I suppose.

Which do you prefer? Let me know in the comments below!

If you’d like to learn more about Go, you can find my posts on Google’s programming language here!

9 thoughts on “Testing a Cobra CLI in Go

    1. The example in the post above isnt exactly the best thing to test since its only logging to the console. Instead, imagine if you had the following:

      package cmd
      
      import (
      	"fmt"
      	"github.com/spf13/cobra"
      )
      
      func doSomething(cmd *cobra.Command, args []string) {
      	return 1+1;
      }
      
      // caneCmd represents the cane command
      var caneCmd = &cobra.Command{
      	Use:   "cane",
      	Short: "Ponder mysteries of the Horadrim",
      	Long:  `Ponder mysteries of the Horadrim`,
      	Run: doSomething,
      }
      
      func init() {
      	rootCmd.AddCommand(caneCmd)
      }
      

      This function is really simple, but an excellent example of something that makes sense to test.

      In this example, you can create a new test file and write assertions on the `doSomething` function.

      For example:

      func TestDoSomething(t *testing.T) {
          got := doSomething(-1)
          if got != 2 {
              t.Errorf("Test failed, expected 2", got)
          }
      }
      

      Hope this helps!

        1. Since that’s just a pointer to a cobra command, you can provide a `nil` value instead. You don’t HAVE to pass anything in that place, and if your `doSomething` function doesn’t need the command, I’d recommend not passing it all together.

          1. Not helpful as a real command will use cmd for accessing flags and probably other information which needs to be provided for the command.

          2. You can also parse all that information in the cobra callback and then call your function with the relevant flags/etc.

Leave a Reply

Your email address will not be published. Required fields are marked *