Slide 1

Slide 1 text

Tour Of Testing in 2018 golang.tokyo #17, AUG 21 2018 Yoichiro Shimizu freee k.k. @budougumi0617

Slide 2

Slide 2 text

● 清水 陽一郎 @budougumi0617 ● freee 株式会社 ○ Backend / Desktop-App Engineer ○ Go / Ruby on Rails / .NET ● golang.tokyo 運営 ● Blog ○ https://budougumi0617.github.io/ ● 趣味 : アウトプット駆動学習 自己紹介

Slide 3

Slide 3 text

About freee SECTION ZERO

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

5 PRODUCTS

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Introduction SECTION ONE

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Today's goal ● go testコマンド /testing.T を改めて確認 ○ Go1.10までの仕様を復習 ● ベタープラクティスをまとめる ● ひとつでも多く新しい発見をしてもらう 01. How to study Go

Slide 10

Slide 10 text

今回の発表では触れないこと ● テストユーティリティ ○ testing/{quick, iotest}, net/http/httptest ● ベンチマーク ○ go test -bench ○ testing.B ● Examples 05. Today’s summary

Slide 11

Slide 11 text

Start and Execute test SECTION TWO

Slide 12

Slide 12 text

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型

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

● 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

Slide 16

Slide 16 text

● 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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

● T.Parallel()メソッドを呼ぶテストケースは並行実行 ● -parallel nオプションのnで同時実行数を制御 ● デフォルトの同時実行数は $GOMAXPROCS -parallel nオプションで最大並行実行数を制御する 02. Start and Execute test

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

● 普段は”-run”だが正式には”-test.run” ○ 他のオプションも”-test.xxx” ● dlv test -- -test.run TestCreateTempFile デバッガ(delve)でテストを実行する 02. Start and Execute test

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

How to write test SECTION THREE

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

● type testing.TB interface ○ testing.T / testing.Bの共通メソッド ● Goのテストはこのinterfaceに定義されているメソッドを 使ってテストの正否を判定する。 testing.TBインターフェースの主なメソッド 03. How to write test

Slide 26

Slide 26 text

● TB.Error / TB.Errorf ○ テストの失敗が記録される ○ 後続処理は継続される ● TB.Fatal / TB.Fatalf ○ 失敗が記録され、テストケースが即時終了する。 ○ その場で宣言済みのdefer処理も開始される testing.TBインターフェースの主なメソッド 03. How to write test

Slide 27

Slide 27 text

● TB.Fail ○ TB.Error / TB.Errorf が内部的に呼んでいる ● TB.FailNow ○ TB.Fatal / TB.Fatalf が内部的に呼んでいる testing.TBインターフェースの主なメソッド 03. How to write test

Slide 28

Slide 28 text

● TB.SkipNow / TB.Skip ○ テストケースを無効化する ● TB.Log / TB.Logf ○ -vオプション、あるいはFailしたときにログ出力 ○ ベンチマーク時は常に出力 testing.TBインターフェースの主なメソッド 03. How to write test

Slide 29

Slide 29 text

● 検証・前処理などを共通化したいときがしばしばある ○ Ex: func ValidateModel( t *testing.T, a, b Model ) ● サブメソッド内でHelper()メソッドを呼んでおく ● サブメソッド内でFailしても呼び出し元の情報が出力 testing.TB.Helper メソッド (Go 1.9~) 03. How to write test

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

● 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

Slide 33

Slide 33 text

-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

Slide 34

Slide 34 text

エラーは無視しない 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

Slide 35

Slide 35 text

エラーは無視しない 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

Slide 36

Slide 36 text

● expected? NO! ● actual? NO! ● 変数名は短いほうがGo way want / got 03. How to write test

Slide 37

Slide 37 text

%#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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Get coverage SECTION FOUR

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

● 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

Slide 42

Slide 42 text

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中にスクリプトを 呼ぶだけで結果を送信できる

Slide 43

Slide 43 text

Today’s Summary SECTION FIVE

Slide 44

Slide 44 text

Today's goal ● go testコマンド /testing.T を改めて確認 ○ Go1.10までの仕様を復習 ● ベタープラクティスをまとめる ● ひとつでも多く新しい発見をしてもらう 05. Today’s summary

Slide 45

Slide 45 text

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