概要
以下、よく忘れるので自分用のメモです。
Goには標準ライブラリにアプリケーション引数を処理してくれる flag パッケージがあります。
flag package - flag - Go Packages
通常は以下のように
opt1 := flag.Int("opt1", 100, "option 1") flag.Parse()
すると、勝手にコマンドライン引数を処理してくれてとっても便利。
ですが、たまにユニットテストでも使えるようにしたい場合とかがあります。
そんなときに flag.FlagSet を利用すると便利です。
以下、サンプル。
サンプル
普通にflagパッケージを利用
package main import ( "flag" "fmt" ) func main() { var ( a = flag.Int("a", 0, "") b = flag.String("b", "", "") c = flag.Bool("c", false, "") ) flag.Parse() fmt.Printf("a=%v\tb=%v\tc=%v\n", *a, *b, *c) }
flag.Parse() を呼び出すと自動で os.Args[1:] の値を使ってパースしてくれます。
flag.FlagSet を利用
package main import ( "flag" "fmt" "os" ) func main() { var ( flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError) a = flags.Int("a", 0, "") b = flags.String("b", "", "") c = flags.Bool("c", false, "") ) flags.Parse(os.Args[1:]) fmt.Printf("a=%v\tb=%v\tc=%v\n", *a, *b, *c) }
flag.FlagSet の Parse メソッドは、自分で値を渡す必要があります。
ユニットテストを書く場合はこっちの方が便利です。
ユニットテスト
以下のような処理があるとして
package args import ( "flag" "io" ) type ( Value struct { A int B string C bool } ) var ( Empty = &Value{0, "", false} Error = &Value{1, "error", false} ) func Parse(options []string) (*Value, error) { if len(options) == 0 { return Empty, nil } var ( flags = flag.NewFlagSet("command", flag.ContinueOnError) a = flags.Int("a", 0, "") b = flags.String("b", "", "") c = flags.Bool("c", false, "") ) flags.SetOutput(io.Discard) err := flags.Parse(options) if err != nil { return Error, err } return &Value{*a, *b, *c}, nil }
それのユニットテストを以下のようにかけます。
package args_test import ( "reflect" "strconv" "testing" args "github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest" ) func TestArgs(t *testing.T) { tests := []struct { in []string out *args.Value }{ {[]string{}, args.Empty}, {[]string{"-a"}, args.Error}, {[]string{"-a", "-b"}, args.Error}, {[]string{"-a", "-b", "-c"}, args.Error}, {[]string{"-a", "1"}, &args.Value{1, "", false}}, {[]string{"-a", "1", "-b", "hello"}, &args.Value{1, "hello", false}}, {[]string{"-a", "1", "-b", "hello", "-c"}, &args.Value{1, "hello", true}}, {[]string{"-a", "1", "-b", "hello", "-c", "true"}, &args.Value{1, "hello", true}}, {[]string{"-a", "bbb", "-b", "hello", "-c"}, args.Error}, {[]string{"-a", "bbb", "-b", "hello", "-c", "world"}, args.Error}, } for i, tt := range tests { tt := tt t.Run(strconv.Itoa(i), func(t *testing.T) { sut, err := args.Parse(tt.in) if !reflect.DeepEqual(tt.out, sut) { t.Errorf("[want] %v\t[got] %v (%v)", tt.out, sut, err) } }) } }
ついでに fuzz テスト
上記のようなパターンは fuzzing しやすいのでついでに用意
// # REFERENCES // - https://future-architect.github.io/articles/20220214a/ // - https://qiita.com/s9i/items/de45b820aaeb6597c9a2 package args_test import ( "strconv" "testing" args "github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest" ) func FuzzArgs(f *testing.F) { f.Add(1, "hello") f.Add(10, "helloworld") f.Add(100, "こんにちは世界") f.Add(-1, "こんにちは世界") f.Fuzz(func(t *testing.T, i int, s string) { options := []string{"-a", strconv.Itoa(i), "-b", s, "-c"} sut, err := args.Parse(options) if sut == args.Error { t.Errorf("i=%v, s=%v, err=%v", i, s, err) } }) }
Taskfile.yml
version: "3" tasks: default: cmds: - task: run-normal - task: run-flagset - task: run-unittest - task: run-fuzztest run-normal: dir: normal cmds: - go run main.go -a=1 -b=hello --c run-flagset: dir: flagset cmds: - go run main.go -a=1 -b=hello --c run-unittest: dir: unittest cmds: - go test -count=1 . run-fuzztest: dir: unittest cmds: - go test -fuzz . -fuzztime=5s
実行すると以下のようになります。
gitpod /workspace/try-golang (master) $ task -d examples/singleapp/flag_pkg/ task: [run-normal] go run main.go -a=1 -b=hello --c a=1 b=hello c=true task: [run-flagset] go run main.go -a=1 -b=hello --c a=1 b=hello c=true task: [run-unittest] go test -count=1 . ok github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest 0.002s task: [run-fuzztest] go test -fuzz . -fuzztime=5s fuzz: elapsed: 0s, gathering baseline coverage: 0/4 completed fuzz: elapsed: 0s, gathering baseline coverage: 4/4 completed, now fuzzing with 16 workers fuzz: elapsed: 3s, execs: 318426 (106116/sec), new interesting: 2 (total: 6) fuzz: elapsed: 5s, execs: 536648 (103307/sec), new interesting: 2 (total: 6) PASS ok github.com/devlights/try-golang/examples/singleapp/flag_pkg/unittest 5.119s
上記のサンプルは以下にアップしています。ご参考まで。
try-golang/examples/singleapp/flag_pkg at master · devlights/try-golang · GitHub
参考情報
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。