My External Storage

Aug 19, 2018 - 13 minute read - Comments - go

Goのtestを理解する in 2018 #go

2018年夏(Go1.10)時点でGoのテスト方法をまとめる。
この記事は以下のスライド資料の補足記事になる。

TL;DR

testing パッケージ

https://golang.org/pkg/testing/

まずは基本としてtestingパッケージがある。Goのテストで使うメソッドは基本的にこのpkg配下にある。(HTTP関連のテストで使うhttptestnet/httppkg配下にある。)
Goでテストを書くときはtestingパッケージを使う。

テストはxxx_test.goというファイルで作成する。
この命名規則を守ることでgo buildのときは無視され、go testのときだけビルドされる。

テストメソッドは*testing.T型を引数として、TestXxxという命名規則で書かれる。Testのあとの単語もキャメルケースで命名する必要がある。TestSumは実行されるが、Testsumは実行されない。

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)
	}
}

構造体メソッドのテストを書くときはTestSturct_Methodという命名するのが望ましい(Examplesを書くときとフォーマットを合わせるため)。

テストデータを用意する

テスト用のデータはtestdataディレクトリに配置する。
JSONやバイナリデータなどをテスト結果として用意するときは.golden拡張子でtestataディレクトリに入れておく。

テストを実行する

テストの書き方の前にテストの実行方法をまとめる。テストはgo testコマンドで実行する。

パッケージを指定して実行する

go testコマンドは以下のように実行できる。

# カレントパッケージのテストを実行する
$ go test

# パスで指定されたパッケージのテストを実行する(相対パス指定が可能)
$ go test ./table

# サブパッケージを含めたパスで指定されたパッケージ以下のテストを全て実行する
$ go test ./...

Go1.10から事前にgo vetコマンドが実行されている

Go1.10からはgo testが実行される前にgo vetが実行される。
どうしてもgo vetを通さずテストの結果をみたい場合はgo test -vet=offとする。

Go1.10からテスト結果がキャッシュされている

Go1.10からgo buildの結果がキャッシュされるように、go testの結果もキャッシュされる(つまり実行されない。特定のオプション時は除外)。
キャッシュを削除したいときはgo clean -testcacheコマンドでキャッシュを削除する。
キャッシュを使わずにテストを実行するだけならば-count=1オプションをつけてgo testを実行する。

# 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

特定のテストファイルだけを実行する

同じパッケージのメソッドを対象にした特定のテストファイルを明示的に指定してテストする場合は、テスト対象のファイルも引数に指定する。

# sum.goの中にテストするメソッドが含まれている
$ go test sum_test.go sum.go

後述するpackage XXX_testを使ったテストの外部パッケージ化をしているときは必然的にimportしているのでテストファイルだけで実行できる。

特定のテストケースだけを実行する

特定のテストケースだけ実行する場合は-runオプションで指定することができる(正規表現可)。

# カレントパッケージ配下にある"Minus"というサブテスト(後述)だけを実行するとき
$ go test ./... -v -run /Minus

-parallel nオプションで最大並行実行数を制御する

後述するT.Parallel()メソッドが呼ばれているテストケースは並行に実行される。
同時に実行されるテストケースの数はデフォルトだとGOMAXPROCSの数と等しい。
-parallel nオプションを使うと、nの数で同時実行数を制御できる。

-cpu listオプションでGOMAXPROCSを変更しながらテストする

-cpuオプションは実行時のGOMAXPROCSの数を変更することができる。
オプション引数には数字の羅列を渡すことができ、例えば-cpu 1,2,4とするとGOMAXPROCSを変えながら都合3回テストが実行される。

デバッガ(delve)でテストを実行する

Delveでテストのデバッグ実行をするときは以下のようにオプションを渡す。
普段-runと指定していても、正式なオプションは-test.runなどだったりする。

$ dlv test -- -test.run TestCreateTempFile

CI上でテストを実行する

JUnit形式で結果を出力するとCIの機能でテスト結果を可視化してくれたりする。
CircleCIの場合は公式にやり方が書いてある。

      - run:
          name: Run unit tests
          command: |
            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

サンプルリポジトリでCircleCIを実行した結果はこちら

playgroundでテストを実行する

ちょっと前からPlaygroundでもテストメソッドを簡単に実行できるようになっている。
main()メソッドを宣言せずにコードを書けばよい。

https://play.golang.org/p/Timl696Usih

テストの書き方

テストコードのパッケージ名について

Goは同じディレクトリ以下に複数のpackage名を許さないが、例外的にxxxパッケージとxxx_testパッケージは同じディレクトリに共存できる。
当然異なるパッケージになるので、非公開メソッドなどは参照できなくなるが、循環参照を回避できる。
また、非公開な機能をexport_test.goを介してアクセスすることもできる。

testing.TBインターフェースの主なメソッド

通常のテスト用のtesting.Tとベンチマーク用のtesting.BErrorfなどいくつか共通メソッドを持っている。
それらはtesting.TBインターフェースのメソッドとして定義されている。Goのテストはここで定義されているメソッドを使ってテストの正否を判定する。

  • TB.Error/TB.Errorfはテストの失敗が記録されるが、後続処理は継続される
  • TB.Fatal/TB.Fatalfはテストの失敗が記録されテストケースは終了する。その場で宣言済みのdefer処理なども開始される
  • TB.Failはテストが失敗したことが記録されるが、その後の処理は継続される。TB.Errorなどが内部的に呼んでいる
  • TB.FailNowはその場でテストケースが終了し、defer処理などが開始される。TB.Fatalなどが内部的に呼んでいる
  • TB.SkipNow/TB.Skipはテストがその場でスキップされる。テストケースを一時的に無効にしたいときに使う。
  • TB.Log/TB.Logf-vオプションがついていたとき、テストがFailしたときに出力される。ベンチマーク時は常に出力される。

testing.TB.Helperメソッド

テストケースを複数書いていると、共通処理を外出ししてサブメソッドにすることがしばしばある。
(たとえば構造体を走査しててエラーを判断したり)
そうすると、エラーログがサブメソッド内になってしまう。Go1.9からそれを防ぐTB.Helperメソッドが追加された。
サブメソッド内で同メソッドを呼ぶと、サブメソッド内でFailしても呼び出し元メソッドの情報が出力される。

何もしないサブメソッドと、TB.Helper()メソッドの呼び出すサブメソッドをhelper_test.goファイルに記述し、

package helper_test

import (
	"testing"
)

func errorf(tb testing.TB, want, got int) {
	tb.Errorf("want = %d, got = %d", want, got)
}

func errorfHelper(tb testing.TB, want, got int) {
	tb.Helper()
	tb.Errorf("want = %d, got = %d", want, got)
}

それぞれをsample_test.goファイル内のテストケースから呼び出すと、

package helper_test

import (
	"testing"
)

// Sum returns two int values.
func Sum(a, b int) int {
	return a + b
}

func TestSum(t *testing.T) {
	a, b, want := 1, 2, 4
	if got := Sum(a, b); got != want {
		errorf(t, want, got)
		errorfHelper(t, want, got)
	}
}

TB.Helper()メソッドを呼んだほうだけ呼び出し元のsample_test.goの情報を持ってエラーが出力される。

$ go test
--- FAIL: TestSum (0.00s)
	helper_test.go:8: want = 4, got = 3
	simple_test.go:16: want = 4, got = 3
FAIL
exit status 1
FAIL	github.com/budougumi0617/go-testing/t/helper	0.010s

Table Driven Test

Goではテーブルを使ってテストを書くことが推奨されている。

  • テーブルでテストを用意することで、テーブル内容を確認すれば評価済みの入出力セットをすぐ確認することができて見通しが良い。
  • また、バグが発見されたときはテーブルに「バグ発生時の入力とあるべき出力」を追加するだけでTDDがスタートできる。

    func TestSum(t *testing.T) {
      tests := []struct {
          a, b, want int
      }{
          {0, 1, 1},
          {-1, -1, -2},
          {-3, 2, -1},
      }
      for _, tt := range tests {
          if got := parallel.Sum(tt.a, tt.b); got != tt.want {
              t.Fatalf("want = %d, got = %d", tt.want, got)
          }
      }
    }

for _, tt := range tests

標準パッケージで確認すると、テーブル駆動テストをするときはtestsで宣言してループをttで流していることが多いので、それに従って書くことが多い。(for _, test := range testsも多いが、testは長いのでttを選ぶ。)

Sub Test(testing.T.Run)

testing.T.Runメソッドを使うとサブテスト書くことができる。

  func TestSum(t *testing.T) {
      tests := []struct {
          name       string
          a, b, want int
      }{
          {"Simple", 0, 1, 1},
          {"Minus", -1, -1, -2},
          {"Both", -3, 2, -1},
      }
      for _, tt := range tests {
          t.Run(tt.name, func(t *testing.T) {
              if got := parallel.Sum(tt.a, tt.b); got != tt.want {
                  t.Fatalf("want = %d, got = %d", tt.want, got)
              }
          })
      }
  }

テーブルテストとサブテストを組み合わせることで以下の効果が得られる。

  • テーブルのテストケースそれぞれでテストの可否を判断することができる
  • 後述するtesting.T.Parallelメソッドを使うことで並行実行ができる
  • -test.runオプションでサブテストそれぞれを個別に実行することができる

サブテストの名前はtesting.T.Runの第一引数に渡した文字列になる。スペースを含んだ文字列は_で置換されるので、使わないほうがよい。
「このテストケースでは何をテストしている(テストしたい)のか?」がわかりやすいようにtable要素で名前をつけたほうがテストコードの可読性が高い。

tests := []struct {
  name string
	// ...
}{}

for _, tt := range tests {
	t.Run(tt.name, func(t *testing.T) {
		// ...
	})
}

冒頭のTestSumを実行すると以下のようになる。

$ go test ./... -v
=== RUN   TestSum
=== RUN   TestSum/Simple
=== PAUSE TestSum/Simple
=== RUN   TestSum/Minus
=== PAUSE TestSum/Minus
=== RUN   TestSum/Both
=== PAUSE TestSum/Both
=== CONT  TestSum/Simple
=== CONT  TestSum/Both
=== CONT  TestSum/Minus
--- PASS: TestSum (0.00s)
    --- PASS: TestSum/Simple (0.00s)
    --- PASS: TestSum/Both (0.00s)
    --- PASS: TestSum/Minus (0.00s)
PASS
ok  	github.com/budougumi0617/go-testing/t/parallel	0.009s

-runオプションで特定のサブテストだけ実行する例は以下になる。

$ go test ./... -v -run Sum/Both
=== RUN   TestSum
=== RUN   TestSum/Both
=== PAUSE TestSum/Both
=== CONT  TestSum/Both
--- PASS: TestSum (0.00s)
    --- PASS: TestSum/Both (0.00s)
PASS
ok  	github.com/budougumi0617/go-testing/t/parallel	0.011s

wantとgot

expectactualは単語が長いので、wantgotで書くほうがベターに思える。

エラーは無視しない

ある一つのメソッドのテストを一つのテーブルに書いていると、以下のようなテストも同じテーブルに入れたくなる。

  • 通常ケースのテスト(エラーが出ないテスト)
  • ある特定のエラーがでたことを確認するテスト

その場合はテストケース内でエラーの有無をフラグとして検証条件を増やす。

  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")},
  }
  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 has error %#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)
          }
      })
  }

エラーの有無だけではなく期待するエラーかも確認するのが望ましい。

Errorfのテンプレート文字列

文字列を出力するときは%#vを使ったほうが良い(ときもある)。微妙に空白が混ざっていたとしても可視化できる。

package main

import (
	"fmt"
)

func main() {
	spaces := "      \n "
	fmt.Printf("spaces = %v\n", spaces)  // spaces =
	fmt.Printf("spaces = %#v\n", spaces) // spaces = "      \n "
}

testing.T.Parallelメソッドで並行実行するテストを書く

Goのテストは通常逐次的に実行される。並行実行オプションをつけて実行してもそれぞれのテストケースは逐次的にされる。
テストケースの中でT.Parallel()メソッドが呼ばれているテストケースのみが並行に実行される。
各サブテストを並行実行したい場合は、testing.T.Run()メソッドで呼ぶfunc(*testign.T)メソッド内で呼ぶ。

  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)
          }
      })
  }

ループ変数のttをローカル変数で補足するのを忘れないこと。

TestMain(m *testing.M)メソッドで前処理を定義する

パッケージのテストを実行する前後で任意の処理を実行してからテストを行いたいときはTestMain(m *testing.M)を宣言しておく。
m.Run()前後に処理を記述することで、そのパッケージのテストの実行前後に処理を割り込ませることができる。

func TestMain(m *testing.M) {
	func() {
		fmt.Println("Prepare test")
	}()
	ret := m.Run()
	func() {
		fmt.Println("Teardown test")
	}()
	os.Exit(ret)
}
$ go test
Prepare test
PASS
Teardown test
ok  	github.com/budougumi0617/go-testing/t/testmain	0.009s

当然(?)だが、特定のテストファイルのみを引数に実行した場合はTestMainは実行は実行されない。

# TestMainはmain_test.goに含まれている
$ go test sumsub_test.go -v
=== RUN   TestSum
...
    --- PASS: TestSub/Both (0.00s)
PASS
ok  	command-line-arguments	0.011s

# TestMainを含んだmain_test.goと一緒に実行する
go test sumsub_test.go main_test.go -v
Prepare test
=== RUN   TestSum
...
    --- PASS: TestSub/Both (0.00s)
PASS
Teardown test
ok  	command-line-arguments	0.009s

ビルドタグでテストケースを分類する

go buildと同じように、go testもビルドタグを使うことができる。
テストファイルごとに適切なビルドタグを指定しておけば、ビルドタグが有効なときだけ実行されるテストケースを宣言しておくことができる。

-tags integrationオプションが付与されたときだけ有効なテストファイルを作成し、

// +build integration

package tags_test

import (
	"testing"

	"github.com/budougumi0617/go-testing/t/tags"
)

func TestSub(t *testing.T) {
	// some testing...
}

通常のファイルは-tags integrationオプション有効時に無効にしておけば、

// +build !integration

package tags_test

import (
	"testing"

	"github.com/budougumi0617/go-testing/t/tags"
)

func TestSum(t *testing.T) {
	// some testing...
}

複数目的のテストを分類して実行できる。

$ go test -v
=== RUN   TestSum
--- PASS: TestSum (0.00s)
PASS
ok  	github.com/budougumi0617/go-testing/t/tags	0.009s

$ go test -v -tags integration
=== RUN   TestSub
--- PASS: TestSub (0.00s)
PASS
ok  	github.com/budougumi0617/go-testing/t/tags	0.009s

テストカバレッジを集計する

Goは標準機能でテストカバレッジを計測することができる。
Go1.10からは複数パッケージのカバレッジも簡単に取得できるようになった。

単純にコマンドライン上に計測結果を出力したいならばgo testコマンドに-coverオプションをつけて実行すればよい。

$ go test -cover ./...

HTMLにカバレッジを保存する

go toolと組み合わせるとカバレッジの計測結果を -covermode=count(atomic)オプションをつけておくと各コード行の実行回数も計測できる。

$ go test ./... -covermode=count -coverprofile=c.out
$ go tool cover -html=c.out -o coverage.html

CIと組み合わせる

たとえばCircle CIならばCI中の成果物を保存しておく設定があるので、これで前述のHTMLファイルを保存しておく。

- store_artifacts:
    path: /code/test-results
    destination: prefix

Codecovと連携するならばCI中で以下のようにスクリプトを呼ぶだけで結果を送信できる。

$ go test ./... -coverprofile=coverage.txt -covermode=count
$ bash <(curl -s https://codecov.io/bash)

サンプルリポジトリを実行したCodecovの結果はこちら。

2019/10追記

この記事をまとめた際はGo1.10だった。Go1.11からGo1.13で追加・変更されたGoの仕様を以下にまとめた。

関連

TODO ベンチマーク、サブパッケージなどを別記事にまとめる。

関連記事