DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Golangでtestingことはじめ(1)〜testingパッケージを使ったユニットテスト〜

こんにちは。
Golangが一般的に使われるようになってきてもう久しいですね。
最近作られたSWET製のツールでも、Golangを採用したものがあります。
そこで、Golangの標準テストパッケージtestingやその他についてまとめたいと思います。

今回から3回にわたり、

を紹介します。

この記事を読んで一通りGolangでテストがかけるようになると嬉しいです。

この文章中に登場するサンプルは GitHub にありますので、実際に動作させることが可能です。

$ go get github.com/duck8823/sample-go-testing
$ cd $GOPATH/src/github.com/duck8823/sample-go-testing
$ git checkout refs/tags/blog
$ dep ensure # 依存パッケージのダウンロード

なお、文章中のコマンドは全てバージョン1.9.2での動作を確認しています。

testingパッケージ

testingパッケージはGolangが標準で提供している、テスト全般を支援するパッケージです。
ベンチマークやカバレッジ、標準出力のテストなどカバーしている範囲は広く、サードパーティ製のテストフレームワークに頼らずともテストが記述できます。 標準なのでGolangをインストールしたら使えます。パッケージマネージャなどで指定する必要はありません。
以下のtypeについてユニットテストを書いてみましょう。

animals/animal.go

package animals

import (
    "fmt"
    "github.com/duck8823/sample-go-testing/foods"
)

type Duck struct {
    name string
}

func NewDuck(name string) *Duck {
    return &Duck{name}
}

func (duck *Duck) Say() string {
    return fmt.Sprintf("%s says quack", duck.name)
}

func (duck *Duck) Eat(food foods.Food) string {
    return fmt.Sprintf("%s ate %s", duck.name, food.Name())
}

foods/food.go

package foods

type Food interface {
    Name() string
}

type Apple struct {
    cultivar string
}

func NewApple(cultivar string) *Apple {
    return &Apple{cultivar}
}

func (apple *Apple) Name() string {
    return apple.cultivar
}

testing では、 _test.go で終わる名前のファイルにテストコードを書きます。
また、TestXxxで始まる関数についてテストを実行します。

なお、テストコードは対応するソースコードと同一のパッケージにすることでpackage privateな変数や関数を呼び出すことができ、 APIを必要以上公開せずにテストを書くことができます。

animals/animals_test.go

package animals

import "testing"

func TestDuck_name(t *testing.T) {
    duck := &Duck{"tarou"}
    actual := duck.name
    expected := "tarou"
    if actual != expected {
        t.Errorf("got: %v\nwant: %v", actual, expected)
    }
}

パッケージ名_test をテストコードを記述するパッケージ名として利用することもできます。 パッケージ外に公開したAPIを通してのみテストしたい場合は、こちらを利用するとよさそうです。
この場合、テスト対象のパッケージもインポートする必要があります。

animals/animals_01_test.go

package animals_test

import "testing"
import "github.com/duck8823/sample-go-testing/animals"

func TestDuck_Say(t *testing.T) {
    duck := animals.NewDuck("tarou")
    actual := duck.Say()
    expected := "tarou says quack"
    if actual != expected {
        t.Errorf("got: %v\nwant: %v", actual, expected)
    }
}

テストを記述したら実行しましょう。 go testの引数にはテストが記述されたファイルを指定することができます。 テストがプロダクトコードと同一のパッケージの場合、引数にテスト対象のファイルを含める必要があります。

$ go test animals/animals_test.go animals/animal.go

プロダクトコードとテストコードのパッケージを分けてimportしている場合は、単一のファイルでも実行可能です。

$ go test animals/animals_01_test.go

他の言語ではビルドツールやテストランナーを介して実行するものが多い中、 goのサブコマンドとしてtestを実行できることからも、言語として積極的にサポートしているのがわかります。

go testの引数はファイルの他、github.com/duck8823/sample-go-testing/animalsのようにパッケージを指定することもできます。 上記の記述だと冗長的になってしまいますが、パッケージの指定はディレクトリからの相対パスとして記述することもできます。

$ go test ./animals

パッケージングされたアプリケーションを作成している場合は ./... を指定することで全ディレクトリのテストを再帰的に実行してくれます。

$ go test ./...

(1.9より以前のバージョンの場合は)

$ go test $(go list ./... | grep -v /vendor)

実行結果は以下のように表示されます。

ok      command-line-arguments  0.007s

詳細な結果を見たい場合は、-vオプションが有効です。

$ go test -v animals/animals_01_test.go
=== RUN   TestDuck_Say
--- PASS: TestDuck_Say (0.00s)
PASS
ok      command-line-arguments  0.007s

アサーション

標準のtestingパッケージでは、アサーション用の関数が用意されていません。
Errorf などの関数を利用し、自分でエラーメッセージを書く必要があります。

duck := animals.NewDuck("tarou")
actual := duck.Say()
expected := "tarou says quack"
if actual != expected {
    t.Errorf("got: %v\nwant: %v", actual, expected)
}

testify のようなサードパーティ製のアサーションライブラリも存在しています。
これらのライブラリを利用することで、記述が容易になります。

import "github.com/stretchr/testify/assert"
duck := animals.NewDuck("tarou")
actual := duck.Say()
expected := "tarou says quack"
assert.Equals(t, expected, actual)

サブテスト

テストを階層構造にすることができます。
サブテストはテストケース内で t.Run をコールすることで実現できます。
第一引数にはテストケース名、第二引数には無名関数を渡すことができます。

animals/animals_02_test.go

package animals_test

import (
    "github.com/duck8823/sample-go-testing/animals"
    "github.com/duck8823/sample-go-testing/foods"
    "testing"
)

func TestDuck(t *testing.T) {
    duck := animals.NewDuck("tarou")

    t.Run("it says quack", func(t *testing.T) {
        actual := duck.Say()
        expected := "tarou says quack"
        if actual != expected {
            t.Errorf("got: %v\nwant: %v", actual, expected)
        }
    })

    t.Run("it ate apple", func(t *testing.T) {
        apple := foods.NewApple("sunfuji")

        actual := duck.Eat(apple)
        expected := "tarou ate sunfuji"
        if actual != expected {
            t.Errorf("got: %v\nwant: %v", actual, expected)
        }
    })
}

テスト結果は以下のように表示されます。

$ go test -v ./animals/animals_02_test.go 
=== RUN   TestDuck
=== RUN   TestDuck/it_says_quack
=== RUN   TestDuck/it_ate_apple
--- PASS: TestDuck (0.00s)
    --- PASS: TestDuck/it_says_quack (0.00s)
    --- PASS: TestDuck/it_ate_apple (0.00s)
PASS
ok      command-line-arguments  0.006s

ベンチマーク

Golangのtestingパッケージでは、ベンチマークも標準でサポートしており、簡単に記述することができます。 ベンチマークは、関数を以下のように定義することで実現できます。

func BeanchmarkXxx(b *testing.B) {
    for i := 0; i < b.N; i++ {
        //  ここに処理を記述
    }
}

animals/animals_06_test.go

package animals_test

import (
    "github.com/duck8823/sample-go-testing/animals"
    "github.com/duck8823/sample-go-testing/foods"
    "testing"
)

func BenchmarkDuck_Say(b *testing.B) {
    duck := animals.NewDuck("tarou")

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        duck.Say()
    }
    b.StopTimer()
}

func BenchmarkDuck_Eat(b *testing.B) {
    duck := animals.NewDuck("tarou")
    food := foods.NewApple("sunfuji")

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        duck.Eat(food)
    }
    b.StopTimer()
}

ベンチマークの実行は -bench オプションをつけます。

$ go test -v -bench=. ./animals/animals_06_test.go
goos: darwin
goarch: amd64
BenchmarkDuck_Say-4     10000000               139 ns/op
BenchmarkDuck_Eat-4      5000000               407 ns/op
PASS
ok      command-line-arguments  3.965s

出力結果はループの回数とループ1回あたりの処理時間です。 -benchmem オプションをつけることで、メモリに関するベンチマークも取得することができます。

前処理や後処理がある場合は ResetTimerStopTimer 関数を呼び出して正しく測定できるようにしましょう。

func BeanchmarkXxx(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        //  ここに処理を記述
    }
    b.StopTimer()
}

カバレッジ

カバレッジも標準でサポートされています。
-cover オプションをつけることで、テスト実行結果にカバレッジ情報が付加されます。

$ go test -cover ./animals
ok      github.com/duck8823/sample-go-testing/animals   10.014s coverage: 100.0% of statements

カバレッジレポートの出力

カバレッジレポートを出力するためにには -coverprofile オプションを利用しますが、 バージョン1.9(2018/01/08現在)では複数のパッケージにまたがって利用することができません。

$ go test -cover -coverprofile cover.out ./animals ./foods
cannot use test profile flag with multiple packages

1.10からはサポートされるようなので、試してみましょう。

beta版のインストールは以下の手順で行います。

go get golang.org/x/build/version/go1.10beta1
go1.10beta1 download

go コマンドをインストールした go1.10beta1 に置き換えて実行してみましょう。

$ go1.10beta1 test -cover -coverprofile cover.out ./animals ./foods
ok      github.com/duck8823/sample-go-testing/animals   10.009s coverage: 100.0% of statements
ok      github.com/duck8823/sample-go-testing/foods 0.006s  coverage: 100.0% of statements

cover.outの出力結果は以下のようになりました。

mode: set
github.com/duck8823/sample-go-testing/foods/food.go:11.39,13.2 1 1
github.com/duck8823/sample-go-testing/foods/food.go:15.35,17.2 1 1
github.com/duck8823/sample-go-testing/animals/animal.go:12.33,14.2 1 1
github.com/duck8823/sample-go-testing/animals/animal.go:16.32,18.2 1 1
github.com/duck8823/sample-go-testing/animals/animal.go:20.47,22.2 1 1

-coverprofile で出力されるカバレッジレポートは見にくいので、 htmlとして出力しましょう。
カバレッジレポートからhtmlを生成するのも標準機能です。

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

f:id:swet-blog:20180115132650p:plain

最後に

今回は紹介しませんでしたが他にもテストのスキップや、標準出力のテストが容易になるExampleといった魅力的な機能があります。 Golangは 公式ドキュメント も充実しているので、是非ご覧ください。

次回はtestingパッケージを利用して共通処理(Before/After)を実現する方法について紹介します。