8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Go】 ユニットテストでflagへ引数を渡す際のハマりどころ

Posted at

はじめに

はじめまして。Go初学者です。
Gopher道場#8や自身の勉強にて、気づいた学びを共有しています。

今回は、あるCLIツールのユニットテストを実装する際のflag引数の渡し方と、 flag.Parse() の実行位置によってテストが落ちる問題、またその理由についてまとめました。

結論から

  • ユニットテスト時は flag.CommandLine.Set(name, value string) error を利用すると実行時引数を渡す事ができる
  • ただし、func init()flag.Parse() を行っていると、テスト時の実行順序の関係上、期待しない動きをする事がある。

testing

is 何

詳細は公式ドキュメントへ

goのテストを書く際に利用する標準パッケージです。テストは go test コマンドで実行します。
一般的に、テスト対象となるファイル ex) main.go の末尾に _test.goを付けて ex) main_test.go テストコードを記載しています。

main.go
package main

func square(i int) int {
    return i * i
}

func main() {}
main_test.go
package main

import "testing"

func Test_square(t *testing.T) {
	tests := []struct {
		name string
		i int
		want int
	}{
		{name: "1", i: 1, want: 1 },
		{name: "2", i: 2, want: 4 },
		{name: "3", i: 3, want: 9 },
		{name: "4", i: 4, want: 16 },
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := square(tt.i); got != tt.want {
				t.Errorf("square() = %v, want %v", got, tt.want)
			}
		})
	}
}

The Go Playgroundのデフォルトセットにも、サンプルがありますね。

flag

is 何

詳細は公式ドキュメントへ

コマンドラインフラグを扱う際に利用する標準パッケージです。
./square -target=10 といった形で、 -targetに対して値を渡す形でコマンドを実行したい場合に利用します。
以下は、flag.IntVarを利用してコマンドラインフラグを指定し、flag.Parse()を実行することで指定した値を受け取り、パッケージ変数 i へ格納しています。

main.go
package main

import (
	"flag"
	"fmt"
	"os"
)

var (
	i int
)

func init() {
	flag.IntVar(&i, "target", 0, "Enter an integer to the square (not 0)")
}

func square(i int) int {
	return i * i
}

func run() int {
	flag.Parse()
	if i == 0 {
		fmt.Println("input 0")
		return 1
	}
	fmt.Println(square(i))
	return 0
}

func main() {
	os.Exit(run())
}

ユニットテストでflagへ引数を渡す

前述したソースの run() 関数をテストします。
テスト関数内で、 flag.Commandline.Set(name, value string) errorを利用します。
その後、テスト対象の run() 関数内で flag.Parse() が実行される事で、パッケージ変数に値を格納することができます。

main_test.go
func Test_run(t *testing.T) {
	tests := []struct {
		name string
		i    int
		want int
	}{
		{name: "input0", i: 0, want: 1},
		{name: "input1", i: 1, want: 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			flag.CommandLine.Set("target", strconv.Itoa(tt.i)) // -target=iと指定したかの様に設定できる
			if got := run(); got != tt.want {
				t.Errorf("run() = %v, want %v", got, tt.want)
			}
		})
	}

ユニットテスト時のハマリどころ

上記のflagの例ですが、サラっとinit()関数を利用しています。
こちらはパッケージの初期化の際に動く関数で、パッケージ変数の複雑な初期化などに利用します。 詳細は公式ドキュメントへ
今回はコマンドラインフラグ -targetを指定する必要があったため、init()関数に記載しています。
当初は以下の通り init()を記述していました。

main.go
func init() {
	flag.IntVar(&i, "target", 0, "Enter an integer to the square (not 0)")
	flag.Parse()
}

この様に記述した場合、後述する実行順序の関係で、下記の通りtestそのものが動作しなくなってしまいます。

$ go test
flag provided but not defined: -test.timeout
Usage of /var/folders/53/j681dgkd1wb7jjyvg8vkwf8h0000gp/T/go-build009487560/b001/aggreagtemyqiita.test:
  -target int
    	Enter an integer to the square (not 0)
exit status 2
FAIL	aggregate-my-qiita/cmd/aggreagtemyqiita	0.300s

エラー内容を見る限り、どうやらflagパッケージが関係していそうですね。

テストの実行順序について

こちらの記事に詳細が書かれています。
go testを実行する際は、メイン関数そのものを実行しテストフラグを付与する事で、テストを実行しているようです。

また、Go1.13リリースノートに以下の通り記載されていました。

Testing flags are now registered in the new Init function, which is invoked by the generated main function for the test. As a result, testing flags are now only registered when running a test binary, and packages that call flag.Parse during package initialization may cause tests to fail.

テスト用に生成されたメイン関数によって呼び出される新しい Init関数にテストフラグが登録されるようになりました。その結果、テストフラグはテストバイナリの実行時にのみ登録されるようになり、パッケージの初期化中に flag.Parse を呼び出すパッケージではテストが失敗する可能性があります。

つまり、私のハマったポイントとしては、init() 関数に flag.Parse() を記載してしまったため、該当のパッケージのテストにおいて、
テスト用のメイン関数のflagセットの前に flag.Parse() が実行されてしまったため、前述のエラーが出たと解釈しました。
間違っていたらご指摘ください。

おわりに

ハマった事で、 go testが何をしているか、Goの初期化から実行までの順序について学ぶ事ができました。
GoはGoで書かれているので、いずれソースコード上で上記の流れを追ってみたいと思います。

参考

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?