[Golang] import した外部パッケージをモックしてテストする方法を知りたい。できれば testify で。
解決したいこと
Go 言語(以下 Golang)で単体テストをする際に、
import
した外部パッケージの関数(メソッド)の挙動を変更させたい。
問題のアプリは、コマンド作成支援パッケージの「cobra」を使った簡単な Hello, world!
の CLI アプリです。
$ cobra init
後、$ cobra add hello
でコマンドのファイル(./cmd/root.go
, ./cmd/hello.go
)を作成し、テスト・ファイル(./cmd/root_test.go
, ./cmd/hello_test.go
)を追加作成しました。
単体テストには testing
とフレームワークの testify
を使っています。
発生している問題
./cmd/root.go
と ./cmd/hello.go
のうち、hello.go
は hello_test.go
のテストでカバレッジは網羅できたのですが、root.go
のテスト root_test.go
で、たどりつけない(網羅できない)箇所があります。
以下の cobra
(&cobra
) 経由の rootCmd.Execute()
の挙動を変えることができず、最後のカバレッジ率を上げられずに困っています。
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "Hello-Cobra",
Short: "This is a sample CLI app.",
Long: "This is a sample of how to use cobra package and unit-testing.",
}
...
func Execute() {
var err = rootCmd.Execute() // ← テスト時、このメソッドの戻り値を nil か -1 で返して動作確認したい
if err != nil {
// テスト時ここまでたどりつけない
fmt.Println(err)
os.Exit(1)
}
}
...
$ go test -v -coverprofile coverage.out ./cmd && go tool cover -func=coverage.out
=== RUN Test_sayHelloWorld
--- PASS: Test_sayHelloWorld (0.00s)
=== RUN Test_initConfig
--- PASS: Test_initConfig (0.00s)
PASS
coverage: 54.5% of statements
ok github.com/KEINOS/Hello-Cobra/cmd 0.006s coverage: 54.5% of statements
github.com/KEINOS/Hello-Cobra/cmd/hello.go:19: init 100.0%
github.com/KEINOS/Hello-Cobra/cmd/hello.go:23: sayHelloWorld 100.0%
github.com/KEINOS/Hello-Cobra/cmd/root.go:18: Execute 0.0% // ← これが上げられない
github.com/KEINOS/Hello-Cobra/cmd/root.go:26: init 100.0%
github.com/KEINOS/Hello-Cobra/cmd/root.go:32: initConfig 100.0%
total: (statements) 60.0%
状況情報
- 質問者 Go 歴: 浅いです。スライスと配列の違いがやっと理解出来て、パッケージをふいんきで使っている程度です。
- OS: Alpine Linux 3.12.0 @ Docker
module github.com/KEINOS/Hello-Cobra
go 1.15
require (
github.com/stretchr/testify v1.6.1 // テストで Assert を使うのに利用
github.com/spf13/cobra v1.1.0 // コマンドの作成と管理をするのに利用
github.com/mitchellh/go-homedir v1.1.0 // cobra が設定ファイルのパスを検知するのに必要
github.com/spf13/viper v1.7.0 // cobra が設定ファイルを読み込むのに必要
)
ソースコード
- オンラインでソースと動作をみる(テストなし) @ Go Playground
.
├── cmd
│ ├── hello.go // ...... hello コマンド本体
│ ├── hello_test.go
│ ├── root.go // ....... コマンド管理のルート・コマンド
│ └── root_test.go
├── go.mod
├── go.sum
└── main.go // ........... root.go を呼び出すだけの本体
package main
import "github.com/KEINOS/Hello-Cobra/cmd"
func main() {
cmd.Execute() // ./cmd/root.go の Execute() が呼ばれる
}
cmd/hello.go
と cmd/root.go
のうち、hello.go
のカバレッジは網羅しているため、問題のコードは root_test.go
になります。
【OK】 な方のコード(hello.go
とそのテストのソースコード)
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// helloCmd represents the hello command
var helloCmd = &cobra.Command{
Use: "hello",
Short: "Says hello to the world.",
Long: "It simply displays Hello, world!",
Run: func(cmd *cobra.Command, args []string) {
sayHelloWorld()
},
}
func init() {
rootCmd.AddCommand(helloCmd)
}
func sayHelloWorld() {
fmt.Println("Hello, world!")
}
package cmd
import (
"bytes"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_sayHelloWorld(t *testing.T) {
var expect = "Hello, world!"
// 標準出力を受け取る準備
var buf bytes.Buffer
var tmpStdout = os.Stdout
var fpRead, fpWrite, _ = os.Pipe() // Pipe のファイルポインタ作成
// defer で標準出力を遅延実行
defer func() {
os.Stdout = tmpStdout
}()
os.Stdout = fpWrite
// 実行
sayHelloWorld()
fpWrite.Close() // クローズしないと永遠に読み込み待ち状態になる
buf.ReadFrom(fpRead) // 出力結果を取得
var actual = strings.TrimRight(buf.String(), "\n")
// 診断
assert.Equal(t, expect, actual)
}
【NG】 な方のコード(root.go
とそのテストのソースコード)
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var cfgFile string
var rootCmd = &cobra.Command{
Use: "Hello-Cobra",
Short: "This is a sample CLI app.",
Long: "This is a sample of how to use cobra package and unit-testing.",
}
func Execute() {
var err = rootCmd.Execute() // ← 問題の箇所
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.Hello-Cobra.yaml)")
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
func initConfig() {
// TODO: 設定ファイルからの読み込み
// 後日実装なので hello と同じ状態にしている
fmt.Println("Read configuration.")
}
package cmd
import (
"bytes"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_initConfig(t *testing.T) {
var expect = "Read configuration."
// 標準出力を受け取る準備
var buf bytes.Buffer
var tmpStdout = os.Stdout
var fpRead, fpWrite, _ = os.Pipe() // Pipe のファイルポインタ作成
// defer で標準出力を遅延実行
defer func() {
os.Stdout = tmpStdout
}()
os.Stdout = fpWrite
// 実行
initConfig()
fpWrite.Close() // クローズしないと永遠に読み込み待ち状態になる
buf.ReadFrom(fpRead) // 出力結果を取得
var actual = strings.TrimRight(buf.String(), "\n")
// 診断
assert.Equal(t, expect, actual)
}
func TestExecute(t *testing.T) {
// ここの作り方がわからない
}
自分で試したこと
...
func TestExecute(t *testing.T) {
// false の場合がテスト出来ていないので、50% しか得られない
Execute()
}
...
おそらく、rootCmd.Execute()
のモックを作成して挙動を変更するのだと思います。
「"golang" testify mock cobra "rootCmd.Execute()"」でググってみたり、以下のサイトを見てみたのですが、どうやら同じパッケージ内の関数に対してのモックの場合のようで、よくわかりませんでした。
- Improving Your Go Tests and Mocks With Testify @ tutorialedge.net
- Goテストの綺麗な書き方 @ Medium
- Testifyでmockを作ってテストを記述してみたメモ @ Qiita
- testify/mockでgolangのテストを書く @ Qiita
それとも root.go
の Execute()
自体をモックするものなのでしょうか。その場合は、rootCmd.Execute()
の挙動が異なった場合のテストにならない気がするのですが。。。よくわかってません。
よろしくお願いいたします。