Summary:-
I have a cobra-cli based golang app which uses viper for config management. I want to make my commands testable. For this I want to inject dependencies to my commands when they are being added to the root command (which happens in the init() of that command's go file; see mycmd.go below) The problem is that viper configs get loaded in initConfig which is run when each command's Execute method is called.
How can I make the commands testable while also using viper for loading config values?
The long version:-
The app has been created using
cobra-cli init --viper
cobra-cli add mycmd
This creates the bare minimum app with following structure.
.
├── cmd
│ ├── mycmd.go
│ └── root.go
├── go.mod
├── go.sum
├── main.go
The main files are
main.go
package main
import "test/cmd"
func main() {
cmd.Execute()
}
the cmd.Execute resides in the root.go file with following content.
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "test",
Short: "A brief description of your application",
Long: `A longer description...`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.test.yaml)")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".test" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".test")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
The init function of this root.go file calls cobra.OnInitialize which sets the passed functions to be run when each command's Execute method is called.
This is how mycmd.go looks.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// mycmdCmd represents the mycmd command
var mycmdCmd = &cobra.Command{
Use: "mycmd",
Short: "A brief description of your command",
Long: `A longer description ...`,
Run: func(cmd *cobra.Command, args []string) {
// Dependency to inject
someObj := viper.GetString("name")
fmt.Println("mycmd called", someObj)
},
}
func init() {
rootCmd.AddCommand(mycmdCmd)
}
To make this testable, I wrapped the mycmdCmd into a function which would accept any dependencies needed by that command to work.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func newMyCmd(someObj string) *cobra.Command {
// mycmdCmd represents the mycmd command
var mycmdCmd = &cobra.Command{
Use: "mycmd",
Short: "A brief description of your command",
Long: `A longer description ...`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("mycmd called", someObj)
},
}
return mycmdCmd
}
func init() {
dependency:=viper.GetString("name")
rootCmd.AddCommand(newMyCmd(dependency))
}
This way I can write tests and pass mock varibles to newMyCmd.
But the problem is when user will run the command (go run ./main.go mycmd --config config.yaml), the rootCmd.AddCommand will be executed as part of init and during that time since viper is not yet initialized, viper.GetString("name") will be empty.
How can I make sure that viper is initialized before addCommand?
I would say that it's not a problem with
init()function execution order.First, multiple
init()functions declared in a single file are executed in the order of their declaration, andinit()functions declared across multiple files in a package are processed in alphabetical order of the file name(Don't bet on it). So in your example,init()inmycmd.gois prior to that inroot.go.Second, even if order is
root.go->mycmd.go, it still would not work. Let's take a look atOnInitialze():The functions passed to
OnInitialize()are not invoked in theOnInitialize(). They would be invoked before each command's Execute method is called(at least afterinit()execution).So if you really need values from Viper in
init()ofmycmd.go, you should better callinitConfig()yourself ininit().