Help us understand the problem. What is going on with this article?

Goのtestingパッケージの基本を理解する

テックタッチアドベントカレンダー11日目を担当する @taisa831 です。10日目は @mxxxxkxxxx「Go言語 ElastiCacheの その前に」でした。575 の 5 が Go に掛かっていていい感じですね!もちろん内容も良い!

本記事では、Gotestingパッケージについて書きます。既存記事を調べてみると、そこそこあるけどそこまで多くはない。重厚な記事もあればあっさりした記事もある。ということで深すぎず浅すぎずを目指そうと思います。

「testing パッケージの基本を理解する」なのでGotesting パッケージ を参考にしました。テストに関しては最初アサーションがないことに戸惑いましたが、慣れたらない方がよく感じてきました。執筆時点でのバージョンはgo1.13.4です。

もっともシンプルなテスト

Goでテストを実行するにはいくつかルールがあります。

  • ファイル名は**_test.goとする
  • 関数はTestCamelCaseのようにTestではじめ後ろはCamelCaseとする
  • testingパッケージを引数で受ける
  • テストファイルはテスト対象ファイルと同一パッケージとする(※テストだけ例外的にpackagename_testとすることも可能)

そして下記のように期待値と実行時の値が違う場合はt.Errorfでエラーを記録します(後述しますが必ずしもt.Errorfである必要はありません)。

func TestAbs(t *testing.T) {
    got := math.Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %f; want 1", got)
    }
}
実行
$ go test
ok      command-line-arguments  4.018s

-vオプションをつけることでより詳細な情報が見られます。

実行
go test -v
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok      command-line-arguments  0.005s

パッケージ/ファイル単位でテスト対象を指定する

パッケージ単位でテスト対象を指定するには以下のようにコマンドを使い分けます。

# カレントディレクトリ
$ go test 

# カレント配下全て
$ go test ./...

# testing パッケージ配下
$ go test testing

# testing/quick パッケージ配下
$ go test testing/quick

# testing 配下全て
$ go test testing/...

# 同じパッケージがテストが対象の場合
$ go test testing_testing.go testing.go

# packgename_test のようにパッケージを分けている場合
$ go test testing_testing.go

Benchmarkを取る

testingパッケージを使ってBenchmarkを取ることもできます。Benchmarkを取るには*testing.Bを使い、-benchオプションを指定して実行します。

func BenchmarkHello(b *testing.B) {
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}

以下の実行時のベンチマークは21,764,674 times at a speed of 54.1 ns per loopとなります。

実行
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/taisa831/sandbox-go/testing
BenchmarkHello-8    21764674            54.1 ns/op
PASS
ok      github.com/taisa831/sandbox-go/testing  1.241s

一部の処理だけBenchmarkを取る

BenchmarkにはStartTimer()StopTime()ResetTimer()が用意されています。これらを利用して一部の処理だけピンポイントでベンチマークをとることもできます。

func BenchmarkBig(b *testing.B) {
    // 重い処理
    Big()
    // リセット
    b.ResetTimer()
    // ここから計測が始まる
    for i := 0; i < b.N; i++ {
        fmt.Sprintf("hello")
    }
}

重い処理は無視して先ほどと同じような結果となるベンチマークが取れました。

実行
go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/taisa831/sandbox-go/testing
BenchmarkBig-8      22457809            54.2 ns/op
PASS
ok      github.com/taisa831/sandbox-go/testing  14.274s

Exampleテスト

testingパッケージには少し特殊なものとして出力をテストするExampleテスト機能があります。Exampleテストは**_test.go内で実行することができます。出力をチェックするにはOutput:Unordered output:を利用します。

Output:

Output: helloと書くことでテストができます。例えばOutput: helleに変えるとテストでエラーとなります。

func ExampleHello() {
    fmt.Println("hello")
    // Output: hello
}
実行
go test -v
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
PASS
ok      github.com/taisa831/sandbox-go/testing  0.005s

複数行出力した結果のテストも可能です。

func ExampleSalutations() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // Output:
    // hello, and
    // goodbye
}
実行
go test -v
=== RUN   ExampleSalutations
--- PASS: ExampleSalutations (0.00s)
PASS
ok      github.com/taisa831/sandbox-go/testing  0.005s

Unorderd output:

Unordered outputはその名の通りオーダーを無視して出力を検証してくれます。

func ExamplePerm() {
    for _, value := range rand.Perm(5) {
        fmt.Println(value)
    }
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}
実行
go test -v
=== RUN   ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
ok      github.com/taisa831/sandbox-go/testing  0.005s

ExampleテストはGoDocにも利用でき、下記のような命名規則があります。

  • func Example() { ... }
    • GoDocPackageに出る
  • func ExampleF() { ... }
    • GoDocFunctionに出る
  • func ExampleT() { ... }
    • GoDocTypeに出る
  • func ExampleT_M() { ... }
    • GoDocTypeMethodに出る

更に上記の単位で細かくExampleテストを分けて書きたい場合は_suffixのように小文字で記述します。

  • func Example_suffix() { ... }
  • func ExampleF_suffix() { ... }
  • func ExampleT_suffix() { ... }
  • func ExampleT_M_suffix() { ... }

GoDocExampleを出力してみる

実際にどのようにGoDocに出るかイメージしにくいのでサンプルを作成して確認してみます。ここまではテストファイルしかなかったので、別途サンプル実装とテストを書いて実行してみました。

person.go
// Package person
package person

type Person struct {
    firstName string
    lastName  string
}

// NewPerson
func NewPerson(firstName string, LastName string) *Person {
    return &Person{firstName: firstName, lastName: LastName}
}

// GetFirstName
func (p *Person) GetFirstName() string {
    return p.firstName
}

// GetLastName
func (p *Person) GetLastName() string {
    return p.lastName
}
person_test.go
func Example() {
    person := NewPerson("Taro", "Yamada")
    fmt.Println(person.GetFirstName())
    // output: Taro
}

func ExampleNewPerson() {
    person := NewPerson("Taro", "Yamada")
    fmt.Println(person.GetFirstName())
    // output: Taro
}

func ExamplePerson_GetFirstName() {
    person := NewPerson("Taro", "Yamada")
    fmt.Println(person.GetFirstName())
    // output: Taro
}

func ExamplePerson_GetLastName() {
    person := NewPerson("Taro", "Yamada")
    fmt.Println(person.GetLastName())
    // output: Yamada
}

上記サンプルコードをGoDocに出力してみます。GoDocが入っていない場合はgo getしてからコマンドを実行しlocalhost:6060にアクセスします。Packagesにアクセスすると自分のパッケージ情報が見られます。

$ go get golang.org/x/tools/cmd/godoc
$ godoc -http=:6060
# localhost:6060にアクセス
  • Example()Packageに表示される

スクリーンショット 2019-12-11 11.28.48.png

  • ExampleNewPerson()Typeに表示される

スクリーンショット 2019-12-11 11.29.09.png

  • ExamplePerson_GetFirstName()ExamplePerson_GetLastNamefuncに表示される

スクリーンショット 2019-12-11 11.29.29.png

スクリーンショット 2019-12-11 11.29.35.png

カバレッジをとる

上記サンプルにテストを追加しカバレッジをとってみます。カバレッジは-coverオプションで簡単にみることができます。ちなみにExampleテストだけでもカバレッジが出るようです。

実行
go test -v -cover
=== RUN   Example
--- PASS: Example (0.00s)
=== RUN   ExampleNewPerson
--- PASS: ExampleNewPerson (0.00s)
=== RUN   ExamplePerson_GetFirstName
--- PASS: ExamplePerson_GetFirstName (0.00s)
=== RUN   ExamplePerson_GetLastName
--- PASS: ExamplePerson_GetLastName (0.00s)
PASS
coverage: 100.0% of statements
ok      github.com/taisa831/sandbox-go-impl/person  0.005s

Exampleテストだけだと中途半端感があるので通常のテストに変えてみます。好みもあると思いますがGolandVSCodeの機能を使うと簡単にテストコードの枠組みが出力できるのでかなり楽にテストが作成できます。

func TestNewPerson(t *testing.T) {
    type args struct {
        firstName string
        LastName  string
    }
    tests := []struct {
        name string
        args args
        want *Person
    }{
        {
            "NewPerson",
            args{"Taro", "Yamada"},
            &Person{firstName: "Taro", lastName: "Yamada"},
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := NewPerson(tt.args.firstName, tt.args.LastName); !reflect.DeepEqual(got, tt.want) {
                t.Errorf("NewPerson() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestPerson_GetFirstName(t *testing.T) {
    type fields struct {
        FirstName string
        LastName  string
    }
    tests := []struct {
        name   string
        fields fields
        want   string
    }{
        {
            "GetFirstName",
            fields{FirstName:"Taro", LastName:"Yamada"},
            "Taro",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            p := &Person{
                firstName: tt.fields.FirstName,
                lastName:  tt.fields.LastName,
            }
            if got := p.GetFirstName(); got != tt.want {
                t.Errorf("GetFirstName() = %v, want %v", got, tt.want)
            }
        })
    }
}

func TestPerson_GetLastName(t *testing.T) {
    type fields struct {
        FirstName string
        LastName  string
    }
    tests := []struct {
        name   string
        fields fields
        want   string
    }{
        {
            "GetLastName",
            fields{FirstName:"Taro", LastName:"Yamada"},
            "Yamada",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            p := &Person{
                firstName: tt.fields.FirstName,
                lastName:  tt.fields.LastName,
            }
            if got := p.GetLastName(); got != tt.want {
                t.Errorf("GetLastName() = %v, want %v", got, tt.want)
            }
        })
    }
}

同じようにカバレッジが出せました。

実行
go test -v -cover
=== RUN   TestNewPerson
=== RUN   TestNewPerson/NewPerson
--- PASS: TestNewPerson (0.00s)
    --- PASS: TestNewPerson/NewPerson (0.00s)
=== RUN   TestPerson_GetFirstName
=== RUN   TestPerson_GetFirstName/GetFirstName
--- PASS: TestPerson_GetFirstName (0.00s)
    --- PASS: TestPerson_GetFirstName/GetFirstName (0.00s)
=== RUN   TestPerson_GetLastName
=== RUN   TestPerson_GetLastName/GetLastName
--- PASS: TestPerson_GetLastName (0.00s)
    --- PASS: TestPerson_GetLastName/GetLastName (0.00s)
PASS
coverage: 100.0% of statements
ok      github.com/taisa831/sandbox-go-impl/person  0.006s

どこのコードをテストが通っているかを確認したい場合にはhtmlに変換して確認することももちろんできます。

実行
$ go test -coverprofile=cover.out
PASS
coverage: 100.0% of statements
ok      github.com/taisa831/sandbox-go-impl/person  0.006s

$ go tool cover -html=cover.out -o cover.html
$ open cover.html

HTML出力結果

スクリーンショット 2019-12-11 13.32.22.png

テストをスキップする

下記の例では--shortオプションと組み合わせて使っていますが、実行するとtesting.Short()trueとなりスキップされます。ショートモードではこのテストは実行不要だという時などに使います。

func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
}
実行
go test --short -v
=== RUN   TestTimeConsuming
--- SKIP: TestTimeConsuming (0.00s)
    testing_test.go:78: skipping test in short mode.
PASS
ok      github.com/taisa831/sandbox-go/testing  0.005s

似た用途のメソッドはtesingパッケージのTBインターフェースに定義されているのであげておきます。基本的にはErrorErrorfを使うことが多いと思います。

  • Error(args ...interface{})
    • ログを出力してエラーをマークする
  • Errorf(format string, args ...interface{})
    • フォーマットログを出力してエラーをマークする
  • Fail()
    • テストにエラーをマークする
  • FailNow()
    • テストにエラーをマークしてそのテストを終了する
  • Failed() bool
    • テストがエラーかをチェックする
  • Fatal(args ...interface{})
    • エラーを出力してそのテストを終了する
  • Fatalf(format string, args ...interface{})
    • フォーマットエラーを出力してそのテストを終了する
  • Log(args ...interface{})
    • ログを出力する
  • Logf(format string, args ...interface{})
    • フォーマットログを出力する
  • Skip(args ...interface{})
    • ログを出力してスキップをマークしてそのテストを終了する
  • SkipNow()
    • スキップをマークしてそのテストを終了する
  • Skipf(format string, args ...interface{})
    • フォーマットログを出力してスキップをマークしてそのテストを終了する
  • Skipped() bool
    • テストがスキップかをチェックする

サブテストとサブベンチマーク

1つのテスト関数にサブテストを追加して複数のテストを実行することができます。ベンチマークも同様に可能です。サブテストがすべて終わるまでt.Logf("%s", "finished")は呼ばれないのでteardown処理を入れることも可能です。

func TestFoo(t *testing.T) {
    t.Run("A=1", func(t *testing.T) {
        got := math.Abs(-1)
        if got != 1 {
            t.Errorf("Abs(-1) = %f; want 1", got)
        }
    })
    t.Run("A=2", func(t *testing.T) {
        got := math.Abs(-1)
        if got != 1 {
            t.Errorf("Abs(-1) = %f; want 1", got)
        }
    })
    t.Run("B=1", func(t *testing.T) {
        got := math.Abs(-1)
        if got != 1 {
            t.Errorf("Abs(-1) = %f; want 1", got)
        }
    })

    t.Logf("%s", "finished")
    // teardown
}
実行
go test -v --run Foo
=== RUN   TestFoo
=== RUN   TestFoo/A=1
=== RUN   TestFoo/A=2
=== RUN   TestFoo/B=1
--- PASS: TestFoo (0.00s)
    --- PASS: TestFoo/A=1 (0.00s)
    --- PASS: TestFoo/A=2 (0.00s)
    --- PASS: TestFoo/B=1 (0.00s)
    testing_test.go:114: finished
PASS
ok      github.com/taisa831/sandbox-go/testing  0.005s

サブテストをパラレルに実行する

サブテスト内でt.Parallel()を呼び出すことでサブテストをパラレルに実行することができます。パラレル動作を確認する為にA-1テストにだけSleepを入れて実行してみます。

func TestGroupedParallel(t *testing.T) {
    tests := []struct {
        Name    string
        Want    bool
        WantErr bool
    }{
        {
            Name:    "test1",
            Want:    true,
            WantErr: false,
        },
    }

    for _, tc := range tests {
        tc := tc // capture range variable
        t.Run(tc.Name, func(t *testing.T) {
            t.Run("A=1", func(t *testing.T) {
                t.Parallel()
                got := math.Abs(-1)
                time.Sleep(2 * time.Second)
                if got != 1 {
                    t.Errorf("Abs(-1) = %f; want 1", got)
                }
                t.Logf("Len=2: %s", time.Now())
            })
            t.Run("A=2", func(t *testing.T) {
                t.Parallel()
                got := math.Abs(-1)
                if got != 1 {
                    t.Errorf("Abs(-1) = %f; want 1", got)
                }
                t.Logf("Len=2: %s", time.Now())
            })
            t.Run("B=1", func(t *testing.T) {
                t.Parallel()
                got := math.Abs(-1)
                if got != 1 {
                    t.Errorf("Abs(-1) = %f; want 1", got)
                }
                t.Logf("Len=2: %s", time.Now())
            })
        })
    }

    t.Logf("%s", "finished")
    // teardown
}

実行結果を見てみるとA-1のテストが最後まで残りfinishedが呼ばれているのが分かります。パラレルテストでも全てのテストが終わるまでfinishedが呼ばれないのでteardown処理をすることができます。

実行
go test -v
=== RUN   TestGroupedParallel
=== RUN   TestGroupedParallel/test1
=== RUN   TestGroupedParallel/test1/A=1
=== PAUSE TestGroupedParallel/test1/A=1
=== RUN   TestGroupedParallel/test1/A=2
=== PAUSE TestGroupedParallel/test1/A=2
=== RUN   TestGroupedParallel/test1/B=1
=== PAUSE TestGroupedParallel/test1/B=1
=== CONT  TestGroupedParallel/test1/A=1
=== CONT  TestGroupedParallel/test1/B=1
=== CONT  TestGroupedParallel/test1/A=2
--- PASS: TestGroupedParallel (2.00s)
    --- PASS: TestGroupedParallel/test1 (0.00s)
        --- PASS: TestGroupedParallel/test1/B=1 (0.00s)
            testing_test.go:149: Len=2: 2019-12-10 19:57:05.678677 +0900 JST m=+0.000551289
        --- PASS: TestGroupedParallel/test1/A=2 (0.00s)
            testing_test.go:141: Len=2: 2019-12-10 19:57:05.678767 +0900 JST m=+0.000641739
        --- PASS: TestGroupedParallel/test1/A=1 (2.00s)
            testing_test.go:133: Len=2: 2019-12-10 19:57:07.681258 +0900 JST m=+2.003074682
    testing_test.go:153: finished
PASS
ok      github.com/taisa831/sandbox-go/testing  2.009s

-runオプションでテスト対象を絞る

-runオプションを指定することでテストファイル内のテスト対象を絞ることができます。

# 全テスト対象
go test -run ''

# Fooにマッチするテストが対象
go test -run Foo

# FooにマッチかつサブテストA=にマッチするテストが対象
go test -run Foo/A=

# サブテストがA=1にマッチするテストが対象
go test -run /A=1

下記を実行するとFoo/A=にマッチするテストだけ実行されてるのが分かります。

実行
go test -v -run Foo/A=
=== RUN   TestFoo
=== RUN   TestFoo/A=1
=== RUN   TestFoo/A=2
--- PASS: TestFoo (0.00s)
    --- PASS: TestFoo/A=1 (0.00s)
    --- PASS: TestFoo/A=2 (0.00s)
PASS
ok      github.com/taisa831/sandbox-go/testing  0.005s

共通処理のエラー箇所を分かりやすく出力する

ちょっとしたことですが、テストで共通処理を呼び出している時、どのテストでエラーが出たかは共通処理のコード情報しか出力されないので通常だと分かりません。そういう時はhelper()を使うと呼び出し元のテスト情報を出力してくれます。

必ずエラーとなるPreTest関数を複数のテスト関数で呼び出して実行して確認してみます。また,TBtestingTBのインターフェースを実装(正確にはcommonが実装してそれを両方とも持っている)しているので*testing.T*testing.Bも渡すことができます。

func PreTest(tb testing.TB) {
    got := math.Abs(-1)
    if got != -1 {
        // t.Helper()
        tb.Errorf("Abs(-1) = %f; want 1", got)
    }
}

通常だとPreTestのエラー箇所である行情報しかでませんが、t.Helper()のコメントアウトを外してもう一回実行してみます。

実行
go test
--- FAIL: TestAbs (0.00s)
    testing_test.go:155: Abs(-1) = 1.000000; want 1
--- FAIL: TestFoo (0.00s)
    testing_test.go:155: Abs(-1) = 1.000000; want 1
    testing_test.go:97: finished

呼び出し元のテスト側の行情報がでるようになりました。

実行
 go test
--- FAIL: TestAbs (0.00s)
    testing_test.go:12: Abs(-1) = 1.000000; want 1
--- FAIL: TestFoo (0.00s)
    testing_test.go:76: Abs(-1) = 1.000000; want 1
    testing_test.go:97: finished
FAIL
exit status 1
FAIL    github.com/taisa831/sandbox-go/testing  2.009s

前処理/後処理をする

*testing.Mを利用すると、対象のテストに対して「前処理」や「後処理」を書くことができます。テスト対象ファイルに下記のように記述することで実行が可能です。また、上記の--runオプションで対象テストを絞っても「前処理」と」後処理」は実行されます。

func TestMain(m *testing.M) {
    println("前処理")

    m.Run()

    println("後処理")
}

テストの最初と最後に「前処理」と「後処理」がプリントされました。

実行
go test -v
前処理
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
=== RUN   TestAbs2
--- PASS: TestAbs2 (0.00s)
=== RUN   TestTimeConsuming
--- PASS: TestTimeConsuming (0.00s)
=== RUN   TestFoo
=== RUN   TestFoo/A=1
=== RUN   TestFoo/A=2
=== RUN   TestFoo/B=1
--- PASS: TestFoo (0.00s)
    --- PASS: TestFoo/A=1 (0.00s)
    --- PASS: TestFoo/A=2 (0.00s)
    --- PASS: TestFoo/B=1 (0.00s)
=== RUN   TestGroupedParallel
=== RUN   TestGroupedParallel/test1
=== RUN   TestGroupedParallel/test1/A=1
=== PAUSE TestGroupedParallel/test1/A=1
=== RUN   TestGroupedParallel/test1/A=2
=== PAUSE TestGroupedParallel/test1/A=2
=== RUN   TestGroupedParallel/test1/B=1
=== PAUSE TestGroupedParallel/test1/B=1
=== CONT  TestGroupedParallel/test1/A=1
=== CONT  TestGroupedParallel/test1/B=1
=== CONT  TestGroupedParallel/test1/A=2
--- PASS: TestGroupedParallel (2.00s)
    --- PASS: TestGroupedParallel/test1 (0.00s)
        --- PASS: TestGroupedParallel/test1/B=1 (0.00s)
            testing_test.go:160: Len=2: 2019-12-10 21:48:38.890123 +0900 JST m=+0.000768137
        --- PASS: TestGroupedParallel/test1/A=2 (0.00s)
            testing_test.go:152: Len=2: 2019-12-10 21:48:38.890183 +0900 JST m=+0.000827182
        --- PASS: TestGroupedParallel/test1/A=1 (2.00s)
            testing_test.go:144: Len=2: 2019-12-10 21:48:40.89206 +0900 JST m=+2.002648644
    testing_test.go:164: finished
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN   ExampleSalutations
--- PASS: ExampleSalutations (0.00s)
=== RUN   ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
後処理
ok      github.com/taisa831/sandbox-go/testing  (cached)

その他

testingパッケージには他にもioテストをしやすくする為のiotestパッケージや、ブラックボックステスト用であるquickパッケージ(ただし凍結されている模様)があります。

おわりに

Goは標準でモックライブラリも用意しているので組み合わせれば標準でほとんどのテストができます。またhttptestもあるのでe2eテストもできます。これらをうまく使いこなして楽しい開発ライフを送りたいと思います。

余談ですが、本記事を書くにあたりGoDocをいろいろみていたら気になる箇所があったのでGo本体にコミット&レビュー依頼してみたところ、何度かやりとりした後、無事取り込んでくれました。

GitHubではなくGerritを使っているので最初のセットアップは少し面倒ですが、一度セットアップすると以降は簡単にレビュー依頼をすることができます。下記の本家記事と日本語の記事がとても分かりやすいので是非参考にしてみてください。

参考

明日のテックタッチアドベントカレンダーの担当は @takakobem です!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした