Go の Test に対する考え方

  • 487
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Go の Test に対する考え方

この記事は Go Advent Calendar 2013 の 9 日目の投稿です。

今回は、 Go の testing というパッケージの使い方を解説しようと思ったのですが、
それだとつまらなすぎるので、合わせて Go が test というか assert についてどういうスタンスをとっているかを書いてみます。

Go でテスト

さて、「テストのないコードはレガシーコード」などと言われて久しく、様々な言語が test (主に Unittest) について言語レベルでサポートしたり、デファクトなライブラリが確立したりといった状況が、今日では至って普通のこととなっています。

そんな言語や環境で、息をするようにテストを書いてきたみなさんが、はじめて Go でコードを書く時に見るべきは testing パッケージです。

http://golang.org/pkg/testing/

命名規則

では、余計な説明がいらないように Sum() 関数のテストを書き、その実装を sum.go にしてみます。
この場合、テストは TestSum() 関数として sum_test.go に書くというのが Go の命名規則になります。

sum_test.go

package MyMath

import (
    "testing"
)

func TestSum(t *testing.T) {
    actual := Sum(10, 20)
    expected := 30
    if actual != expected {
        t.Errorf("got %v\nwant %v", actual, expected)
    }
}

testing パッケージを import したら、テスト関数を書きます。引数は必ず *testing.T を引数に取るのがルールです。失敗した場合のために、 t.Errorf() にエラーメッセージを記述します。 fmt.Printf() と同じようにプレースホルダが使えます。

実行

実行は go test コマンドを使います。

$ go test sum_test.go
# command-line-arguments
./sum_test.go:8: undefined: Sum
FAIL    command-line-arguments [build failed]

では実装を。

package MyMath

func Sum(i, j int) int {
    return i + j
}
$ go test sum_test.go
ok      command-line-arguments  0.050s

通りました。
(本当はちゃんとディレクトリ構造作ってパスを通す必要があります)

Go には標準 assert がない

さて、ここまでコードを読飛ばさずにきて下さった方は気づいたと思いますが、、

func TestSum(t *testing.T) {
    actual := Sum(10, 20)
    expected := 30
    if actual != expected {
        t.Errorf("got %v\nwant %v", actual, expected)
    }
}

testing.AssertEqual() とかありません、ただの if 文です。
実は、 Go には testing パッケージがありながら、 assert 的なものが標準では提供されていません。
testing にあるのは、基本的にはテストを落とす関数や、 Skip する関数です。
そして、実行のために go test コマンドが標準で提供されています。

充実しまくった標準ライブラリに定評のある Go がいったいどうしたのでしょう?

わかった、作れば良いのか!

「なるほど、 Assert は自分で実装することが推奨されているんだな」
「よし、ならば俺が考える最強の Assert パッケージを」

と思ったあなた、まあそう考えるのが普通かもしれません。
「無ければ作る」が OSS の流儀です。
実際、そう考えて巷?にはいくつかの assert パッケージがあります。

そして、「きっと、誰かが良い感じの assert パッケージを作っているだろう」という調査結果もちらほら。

二つ目の記事でも、以下のように言及されています。

「これはつまり、Goの標準組み込みのtestingパッケージというのはテスティングフレームワークを実装する為の基盤なのだな…というのが僕の理解。」

自分も最初触ってみて、「あー自分で作るのか」と思ったんですが、その前に「なぜ無いのか」を調べてみました。 Go は言語自体や、標準パッケージの開発には、開発者達の強い思想が反映されています。 Assert が無いのにも理由があるはず。

Assert が無い理由

ずばり FAQ にあります。

要するに、「Assert は便利だけど、頼り過ぎてエラーのレポートが適当になる。エラーレポート重要だからきちんと書こう。議論の余地はあるけど、新しい試みとしてやってみる。」とのこと。

さらに詳細が同じく FAQ の別の項にあります。

要するに、「みんなテストを書くためにすぐミニ言語作りたがるけど、必要な機能はだいたい Go 本体に揃ってるから、覚えること増やす必要無い。そうやって自動生成したエラーレポートより、ちゃんと自分で意図を書こう。面倒なのは分かるけど、そのコードに全く詳しくない人や、後のデバッグする時にそのコストは回収できるはず。」といった感じでしょうか。

じゃあ、本家はどうやってるのか。サンプルとして標準パッケージのテストを見てみましょう。

Go の標準パッケージのテスト

適当に net/http あたりを見てみます。

http://golang.org/src/pkg/net/http/serve_test.go#L224

それぞれのケースでちゃんとエラーメッセージを書いてますね。
「何が落ちたか」よりも、「なんで落ちたか」が書かれています。
これは、確かに Assert.Equals() 的なもので自動生成した場合にはできません。
書くのは面倒ですが、はじめてこのコードを見る人でも、きっと意味がわかるでしょう。

次は、 fmt パッケージのテストを見てみます。

http://golang.org/src/pkg/fmt/fmt_test.go#L520

ここでは、 t.Errorf() は、 "want a but b" 的な画一的なものですね。でもこれは、その 前に作られた巨大な促成 struct を range で回した結果です。つまりパラメタライズ的なテストですね。

ちなみに、ちょうどいい例はありませんでしたが、 struct などの同値検査には reflect.DeepEqual() が使えます。

ここまで全て一応標準機能です。

Go Test コマンド

test コマンドはどうでしょう。もちろんテストが実行できるのですが、以下のオプションがあります。

-bench regexp
-benchmem
-benchtime t
-blockprofile block.out
-blockprofilerate n
-cover
-covermode set,count,atomic
-coverpkg pkg1,pkg2,pkg3
-coverprofile cover.out
-cpu 1,2,4
-cpuprofile cpu.out
-memprofile mem.out
-memprofilerate n
-outputdir directory
-parallel n
-run regexp
-short
-timeout t
-v

並列実行や、各プロファイルの取得、以前解説したベンチの取得、 Go1.2 からはカバレッジの取得も追加されています。
基本的な情報は、実行時にフラグを足すだけで取れます。その知識はどのプロダクトを弄っても同じで、Go の実行環境だけで可能です。
(Go は、言語そのものをシンプルに保つ一方、コマンド系は割とどん欲に色々取り入れているイメージがあります。)

Go が選んだもの

Go は、 assert やその他テストを構造化する仕組みなどを標準で提供しない代わりに、最小の API と十分なコマンドを提供することでテストを限りなく「ただの Go プログラム」で書けるようにしています。

これは、今まで Ruby や JS などで、 DSL レベルに整備された潤沢な API を使ってテストを書いてきた人にするとかなり戸惑うところが多いのが事実です。
すぐにでも、俺々モジュールを作ってしまいたくなるところで、自分もその誘惑には何度もかられました。

しかし、せっかく言語が新しいことに挑戦しているので、だったら乗ってみようと思い、 Assert を探すのはやめました。
まだ、そこまで大したものは書いていないから、なんとかなってはいます。コマンドにも特に不足を感じていません。
(とか言っておきながら、 Errorf() のコメントが面倒で結構適当になっている部分はあるのは否定できない。。)

また、これを実践するなかで今までの整備されたテスト環境や、蓄積されたノウハウの良さも見えてくるし、
逆に、過度な部分、あと書き始める前の「今一番アクティブなテスティングフレームワークはなんだろう?」とか「環境はどうやって作るのが今のデファクトなんだろ?」など調べるところから始める部分の煩わしさを再認識したりと、色々思う事があるのでそれだけでも得るものがあるかなと。

ということで、しばらくは基本的には Go 標準の方法でテストを書いていってみようと思っています。

おまけ

ちなみに、 Go にはジェネリクスが無いので、汎用な Assert を作ろうとすると interface + reflect ゴリゴリになって結構めんどうです。
あと、過度な抽象化は Go にはあまり合いません。そういった実装面の理由も、 Assert を提供しない理由のにあるかもしれません。