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?