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

golang 1.4で追加されたtestingの便利機能(テストの初期化とお片づけ)

More than 3 years have passed since last update.

golangのtestingパッケージはシンプル主義のgoならではといった、最小限の機能のみを提供しています。実際のところこれまでも(ほぼ)充分な機能を提供してきたわけですが、テスト前の初期化を明確に定義できないなど、不満もありました。

そういうわけで、1.4からこの不満を解決するtesting.MTestMain(*testing.M)が追加されています。これがかなり最高便利です。

従来のテクニック

当然、これまでもテストの"初期化"や"お片づけ"を書きたいという要求は当然ありました。それに対しては、go testコマンドがファイルをabc...順に読みこむことを利用して、ファイル名を工夫するというちょっと裏ワザ的な手法が一般的でした。
すなわち、最初に実行したいテストが書かれたファイルは「a_test.go」にして、最後に実行したいテストを「z_test.go」とするわけです。この方法はジョークっぽいのですが、実際にgolang 1.3のnet/httpパッケージなど、本流でも盛んに使われているいわば公式テクニックでした。

より良い方法

現在は21世紀なので、もっと良い方法が提供されました。パッケージ内のどこかでTestMain(*testing.M)という関数が定義されていれば、これがgo testコマンドのエントリーポイントとして機能するようになったのです。このtesting.MにはRun()というメソッドが生えていて、これを叩くと今までどおりのユニットテストが走り出します。テストを走らせるタイミングを変えることができるようになったことで、その前後にイロイロな処理を挟むことができるようになりました。
# ちなみにgo testは成否でプロセスのexit codeが変化するので、ケアが必要。

func TestMain(m *testing.M) {
    // ここにテストの初期化処理
    code := m.Run()
    // ここでテストのお片づけ
    os.Exit(code)
}

そういうわけでひと目でテストの初期化と終了処理が判るようになって読みやすくなりましたし、なんかこう納得感のある造りになりました。

もちろん先に紹介したnet/httpパッケージもTestMainを使って書き直されています。

応用編

このtesting.Mオブジェクトのもうひとつ良い所は、テストを繰り返し実行できることです。これまで"環境を変えて同じシナリオテストを繰り返す"みたいなテストケースでは、かなり無理のあるテストを書く必要がありました。たとえば、

func TestAllEnvironments(t *testing.T) {
    for _, env := range environments {
        setEnv(env)
        testA1(t)
        testA2(t)
        // ....
    }
}

func testA1(t *testing.T) {
    // ...
}

func testA2(t *testing.T) {
    // ...
}

のようなケースです。go testから呼ばれる親のTestA1が環境の切替を行いつつ、テストそのものが書かれた関数を叩いていくような構造ですね。このようなテストを書くと、せっかくのtestingパッケージの仕組みに乗り切っていない・・・となんだかしょんぼりします。
しかし、testing.Mの登場で、全て通常通りの"Test*"として扱っても同様なテストを実現できるようになりました。

func TestMain(m *testing.M) {
    for _, env := range environments {
        setEnv(env)
        exit := m.Run()
        if exit != 0 {
            os.Exit(exit)
        }
    }
}

func TestA1(t *testing.T) {
    // ...
}

func TestA2(t *testing.T) {
    // ...
}

このように書くと、TestA1()TestA2m.Run()の回数だけ実行されるので、全ての環境で同じテストを、testingパッケージの仕組みの上に乗せて実現できます。(例は簡略化して書いたんですが、ホントはexitコードにかかわらず全て実行して、最後に死んだほうが良いですね!)

まぁやっぱり上手く扱えないコーナーなケースも思いつくっちゃ思いつくんですが、だいぶ良くなりましたよね。僕は非常に満足しています。
# 特にプロダクションで便利ですね。環境差異チェックをスバッと通せます。

目下、趣味で開発中のSQLクエリビルダのDBとの結合テストにも採用してみました。sqliteとMySQLで同じテストを通しますが、golangのtestingの仕組みの上でシンプルに実装することができたと思います。
https://github.com/umisama/go-sqlbuilder/blob/master/integration_test/integration_test.go#L18-L63

まとめ

golang 1.4からテストの初期化・後片付けや、繰り返しテストをするケースがとても便利になりました。その仕組みである*testing.MTestMain(*testing.M)を紹介しました。

リリースノート、packagesの所で小さく取り上げられてただけなので見落としていたんですが、go generateよりも個人的には使い手のある機能という感じがしてきていて、なんで気づかなかったんだろうという感じです。
特に手元のプロダクションは無理に書いた子がたくさん居るので、古いコードはドンドン置き換えていきたいなぁ。

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