Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Tour of testing in 2018

Tour of testing in 2018

golang.tokyo #17の発表資料です。
https://golangtokyo.connpass.com/event/96200/

Go1.10までのGoのテストの仕様、ノウハウをまとめました。

口頭で説明する内容や、文中のリンクは以下のブログに記載されています。
https://budougumi0617.github.io/2018/08/19/go-testing2018/

Yoichiro Shimizu

August 21, 2018
Tweet

More Decks by Yoichiro Shimizu

Other Decks in Technology

Transcript

  1. Tour Of Testing in 2018 golang.tokyo #17, AUG 21 2018

    Yoichiro Shimizu freee k.k. @budougumi0617
  2. • 清水 陽一郎 @budougumi0617 • freee 株式会社 ◦ Backend /

    Desktop-App Engineer ◦ Go / Ruby on Rails / .NET • golang.tokyo 運営 • Blog ◦ https://budougumi0617.github.io/ • 趣味 : アウトプット駆動学習 自己紹介
  3. Today’s contents About freee 00 Introduction Start and Executed test

    How to write test 01 02 03 04 Get coverage Today’s summary 05
  4. How do you studying test in Go? • テストの書き方どう覚える? •

    毎リリースそれなりに変更・追加がある ◦ https://golang.org/doc/go1.7#testing ◦ https://golang.org/doc/go1.8#testing ◦ https://golang.org/doc/go1.9#test-helper ◦ https://golang.org/doc/go1.10#test 01. How to study Go
  5. Today's goal • go testコマンド /testing.T を改めて確認 ◦ Go1.10までの仕様を復習 •

    ベタープラクティスをまとめる • ひとつでも多く新しい発見をしてもらう 01. How to study Go
  6. Basic and naming 02. Start and Execute test package main

    import "testing" func Sum(a, b int) int { return a + b } func TestSum(t *testing.T) { a, b, want := 1, 2, 3 if got := Sum(a, b); got != want { t.Fatalf("want = %d, got = %d", want, got) } } • import “testing” • ファイル名はxxx_test.go ◦ go testのときだけビルド • メソッド名はTestXxx ◦ Test”s”umは実行されない ◦ TestFunction ◦ TestSturct ◦ TestSturct_Method ◦ TestSturct_Method_cond • 引数は *testing.T型
  7. Test dataを用意する var update = flag.Bool("update", false, "update .golden files")

    func TestToJSON(t *testing.T) { var b bytes.Buffer // Load JSON into b... g, err := ioutil.ReadFile( filepath.Join("testdata", t.Name()+".golden")) if !bytes.Equal(b.Bytes(), g) { t.Errorf("Does not match .golden file") } } • testdataディレクトリに置く ◦ pkgとして認識されない • *.golden 拡張子 ◦ バイナリやJSONなど • 参考 ◦ cmd/gofmt/testdata/ 02. Start and Execute test
  8. Execute test # カレントパッケージのテストを実行する $ go test # パスで指定されたパッケージのテストを実行する(相対パス指定が可能) $

    go test ./table # サブパッケージを含めてテストを全て実行する $ go test ./... # 特定のテストコードだけ実行する(テスト対象も含めて指定する) $ go test sum_test.go sum.go # "Minus"というサブテスト(後述)だけを実行する $ go test ./... -v -run /Minus # Race conditionの検出 $ go test ./... -race • go testコマンド • サブパッケージを含めて実行 • 引数に応じて実行対象を絞れる • レースコンディションの検出 02. Start and Execute test
  9. • Test - Go 1.10 Release Notes ◦ https://golang.org/doc/go1.10#test •

    Go 1.10からテストの実行前にgo vetが実行される • go vetを抑制する / go test -vet=off go vetを抑制する 02. Start and Execute test $ go test github.com/budougumi0617/hogehoge ./foo_test.go:10:2: x declared and not used
  10. • Build and test caching ◦ https://golang.org/cmd/go/#hdr-Build_and_test_caching • Go 1.10からgo

    build/testの結果がキャッシュされる ◦ ベンチマークなど一部のオプション付与時は除外 • キャッシュを削除する / go clean -testcache • キャッシュなしで実行 / go test -count=1 Test Cache 02. Start and Execute test
  11. Test Cache # t/parallel パッケージの結果だけキャッシュされている $ go test ./... ok

    github.com/budougumi0617/go-testing/t 0.009s ok github.com/budougumi0617/go-testing/t/parallel (cached) ok github.com/budougumi0617/go-testing/t/table 0.009s # 全ての結果がキャッシュされている(つまり何も実行していない) $ go test ./... ok github.com/budougumi0617/go-testing/t (cached) ok github.com/budougumi0617/go-testing/t/parallel (cached) ok github.com/budougumi0617/go-testing/t/table (cached) # キャッシュを使わずに全て実行する $ go test ./... -count=1 ok github.com/budougumi0617/go-testing/t 0.011s ok github.com/budougumi0617/go-testing/t/parallel 0.011s ok github.com/budougumi0617/go-testing/t/table 0.010s 02. Start and Execute test
  12. CI上でテストを実行する $ trap "go-junit-report <${TEST_RESULTS}/go-test.out > ${TEST_RESULTS}/go-test-report.xml" EXIT $ go

    test -v ./... | tee ${TEST_RESULTS}/go-test.out • JUnit形式に変換する ◦ https://github.com/jstemmer/go-junit-report • 例えばCircle CIは公式ガイドどおりに ◦ https://circleci.com/docs/2.0/language-go/ 02. Start and Execute test
  13. • type testing.TB interface ◦ testing.T / testing.Bの共通メソッド • Goのテストはこのinterfaceに定義されているメソッドを

    使ってテストの正否を判定する。 testing.TBインターフェースの主なメソッド 03. How to write test
  14. • TB.Error / TB.Errorf ◦ テストの失敗が記録される ◦ 後続処理は継続される • TB.Fatal

    / TB.Fatalf ◦ 失敗が記録され、テストケースが即時終了する。 ◦ その場で宣言済みのdefer処理も開始される testing.TBインターフェースの主なメソッド 03. How to write test
  15. • TB.Fail ◦ TB.Error / TB.Errorf が内部的に呼んでいる • TB.FailNow ◦

    TB.Fatal / TB.Fatalf が内部的に呼んでいる testing.TBインターフェースの主なメソッド 03. How to write test
  16. • TB.SkipNow / TB.Skip ◦ テストケースを無効化する • TB.Log / TB.Logf

    ◦ -vオプション、あるいはFailしたときにログ出力 ◦ ベンチマーク時は常に出力 testing.TBインターフェースの主なメソッド 03. How to write test
  17. • 検証・前処理などを共通化したいときがしばしばある ◦ Ex: func ValidateModel( t *testing.T, a, b

    Model ) • サブメソッド内でHelper()メソッドを呼んでおく • サブメソッド内でFailしても呼び出し元の情報が出力 testing.TB.Helper メソッド (Go 1.9~) 03. How to write test
  18. testing.TB.Helper メソッド (Go 1.9~) 03. How to write test //

    in helper_test.go func errorf(tb testing.TB, want, got int) { tb.Errorf("want = %d, got = %d", want, got) } // in helper_test.go func errorfHelper(tb testing.TB, want, got int) { tb.Helper() tb.Errorf("want = %d, got = %d", want, got) } ------ $ go test --- FAIL: TestSum (0.00s) helper_test.go:8: want = 4, got = 3 simple_test.go:16: want = 4, got = 3 FAIL
  19. Table Driven Test func TestFizzBuzz(t *testing.T) { tests := []struct

    { name string input int want string }{ {name: "Num", input: 2, want: "2"}, {name: "Multi3", input: 3, want: "Fizz"}, {name: "Multi5", input: 5, want: "Buzz"}, ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := FizzBuzz(tt.input) if got != tt.want { t.Errorf("want %d, but %s:", tt.want, got) } } } • テストケースをテーブル化 • 再利用性が高い • エンバグ時にデグレしにくい ◦ テストケースを追加 ◦ リグレッションと同時に実装 • “tests”で回して”tt”で受ける • T.Runでサブテストにする 03. How to write test
  20. • func (t *T) Run(name string, f func(t *T)) bool

    • テスト成否を各テストケースで出力できる • T.Parallel()メソッドで各ケースごとに並行実行できる • -test.runオプションでケースを指定して実行 • テストケースそれぞれに名前をつけられる testing.T.Runでサブテストにする(Go 1.7~) 03. How to write test
  21. -test.run オプションで個別ケース実行 $ go test ./... -v --- PASS: TestFizBuz

    (0.00s) --- PASS: TestFizBuz/Num (0.00s) --- PASS: TestFizBuz/Multi3 (0.00s) --- PASS: TestFizBuz/Multi5(0.00s) PASS $ go test ./... -v -run FizBuz/Multi5 --- PASS: TestFizBuz (0.00s) --- PASS: TestFizBuz/Multi5(0.00s) PASS • -runオプションでケースを指定 • 「何確かめてるの?」問題 ◦ ドメイン知識がいるテスト ケースはケースごとに名前 が付いてるほうが良い ◦ Fail時のログも良好 03. How to write test
  22. エラーは無視しない tests := []struct { name string in int want

    int wantError bool err error }{ { "Basic", 4, 4, false, nil }, { "HasError", -1, 0, true, errors.New("Negative value") }, } • 正常ケースだけ? • エラーが出ることもテストする ◦ 通常ケース ◦ エラーケース ◦ 期待通りのerrorか? 03. How to write test
  23. エラーは無視しない for _, tt := range tests { t.Run(tt.name, func(t

    *testing.T) { pt := we.PositiveInt(tt.in) got, err := pt.Value() if !tt.wantError && err != nil { t.Fatalf("want no err, but %#v", err) } if tt.wantError && !reflect.DeepEqual(err, tt.err) { t.Fatalf("want %#v, but %#v", tt.err, err) } if !tt.wantError && got != tt.want { t.Fatalf("want %q, but %q", tt.want, got) } }) } • 正常ケースだけ? • エラーが出ることもテストする ◦ 通常ケース ◦ エラーケース ◦ 期待通りのerrorか? 03. How to write test
  24. %#vで出力差異をわかりやすく spaces := " \n " // spaces = //

    fmt.Printf("spaces = %v\n", spaces) // spaces = " \n " fmt.Printf("spaces = %#v\n", spaces) • “%v”より”%#v” ◦ (がいいときもある) 03. How to write test
  25. testing.T.Parallelメソッドで並行実行 for _, tt := range tests { tt :=

    tt // Don't forget when parallel test t.Run(tt.name, func(t *testing.T) { t.Parallel() if got := Sum(tt.a, tt.b); got != tt.want { t.Fatalf("want = %d, got = %d", tt.want, got) } }) } • 通常テストは逐次実行 • T.Parallel()が呼ばれているテスト ケースのみが並行に実行される。 • T.Parallel()を呼ぶときはループ変 数をローカル変数で補足するのを 忘れないこと。 03. How to write test
  26. • go toolでカバレッジの計測結果をHTMLに出力できる • -covermode=count(atomic)オプションをつけると各コード 行の実行回数も計測できる。 ◦ $ go test

    ./... -covermode=count -coverprofile=c.out ◦ $ go tool cover -html=c.out -o coverage.html HTMLにカバレッジを保存する 04. Get coverage
  27. CIと組み合わせる 04. Get coverage - store_artifacts: path: /code/test-results destination: prefix

    • Circle CIならばCI中の成果物を保 存する設定があるので、これで前 述のHTMLファイルを保存してお く。 $ go test ./... -coverprofile=coverage.txt \ -covermode=count $ bash <(curl -s https://codecov.io/bash) • CodecovならCI中にスクリプトを 呼ぶだけで結果を送信できる
  28. Today's goal • go testコマンド /testing.T を改めて確認 ◦ Go1.10までの仕様を復習 •

    ベタープラクティスをまとめる • ひとつでも多く新しい発見をしてもらう 05. Today’s summary