Testing a Cobra CLI in Go
Posted on July 18, 2019  (Last modified on December 27, 2022 )
3 minutes • 552 words
This project uses these versions of languages, frameworks, and libraries.
-
go
: 1.16
This tutorial may work with newer versions and possibly older versions, but has only been tested on the versions mentioned above.
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.
- Create a local function and have the anonymous cobra function call the local function.
- 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 !