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/

Eb6be531bcfaa99714d8d3b48665a5a9?s=128

Yoichiro Shimizu

August 21, 2018
Tweet

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. About freee SECTION ZERO

  4. 4 スモールビジネスを、 世界の主役に。 Mission

  5. 5 PRODUCTS

  6. 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
  7. Introduction SECTION ONE

  8. 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
  9. Today's goal • go testコマンド /testing.T を改めて確認 ◦ Go1.10までの仕様を復習 •

    ベタープラクティスをまとめる • ひとつでも多く新しい発見をしてもらう 01. How to study Go
  10. 今回の発表では触れないこと • テストユーティリティ ◦ testing/{quick, iotest}, net/http/httptest • ベンチマーク ◦

    go test -bench ◦ testing.B • Examples 05. Today’s summary
  11. Start and Execute test SECTION TWO

  12. 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型
  13. 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
  14. 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
  15. • 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
  16. • 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
  17. 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
  18. • T.Parallel()メソッドを呼ぶテストケースは並行実行 • -parallel nオプションのnで同時実行数を制御 • デフォルトの同時実行数は $GOMAXPROCS -parallel nオプションで最大並行実行数を制御する

    02. Start and Execute test
  19. • 実行時の$GOMAXPROCSを変更できる。 • オプション引数(list)は数字の羅列 • Ex: -cpu 1,2,4 ◦ $GOMAXPROCSを変えながら都合3回実行

    -cpu listで$GOMAXPROCSを変更しながらテストする 02. Start and Execute test
  20. • 普段は”-run”だが正式には”-test.run” ◦ 他のオプションも”-test.xxx” • dlv test -- -test.run TestCreateTempFile

    デバッガ(delve)でテストを実行する 02. Start and Execute test
  21. • https://play.golang.org/ • 実はそのまま動かせるようになったのは最近 • func main()を宣言せずにコードを書けばよい playgroundでテストを実行する 02. Start

    and Execute test
  22. 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
  23. How to write test SECTION THREE

  24. • 同ディレクトリに複数パッケージは通常許されない • 例外的に”xxx”と”xxx_test”は共存可能 • 非公開メソッドなどは参照できなくなるが、循環参照を回避 できる • export_test.goを使うと… テストコードのパッケージ名について

    03. How to write test
  25. • type testing.TB interface ◦ testing.T / testing.Bの共通メソッド • Goのテストはこのinterfaceに定義されているメソッドを

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

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

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

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

    Model ) • サブメソッド内でHelper()メソッドを呼んでおく • サブメソッド内でFailしても呼び出し元の情報が出力 testing.TB.Helper メソッド (Go 1.9~) 03. How to write test
  30. 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
  31. 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
  32. • 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
  33. -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
  34. エラーは無視しない 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
  35. エラーは無視しない 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
  36. • expected? NO! • actual? NO! • 変数名は短いほうがGo way want

    / got 03. How to write test
  37. %#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
  38. 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
  39. Get coverage SECTION FOUR

  40. • Goは標準機能でテストカバレッジを計測できる • Go1.10からは複数パッケージのカバレッジも簡単に取得でき るようになった ◦ $ go test -cover

    ./… go test -cover 04. Get coverage
  41. • 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
  42. 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中にスクリプトを 呼ぶだけで結果を送信できる
  43. Today’s Summary SECTION FIVE

  44. Today's goal • go testコマンド /testing.T を改めて確認 ◦ Go1.10までの仕様を復習 •

    ベタープラクティスをまとめる • ひとつでも多く新しい発見をしてもらう 05. Today’s summary
  45. スモールビジネスを、 世界の主役に。