Golangでユニットテスト書くテクニック

Goは他のフレームワークにあるような大きなアサーションツールを持っていません。Goでは testing.T オブジェクトのメソッドがテストに使われます。

  • T.Error(args ...interface{}) または T.Error(msg string, args interface{}) はメッセージを受け取ってテストを失敗させるために使用されます
  • T.Fatal(args ...interface{}) または T.Fatal(mst string, args interface{})T.Error() と似ていますがテストが失敗すると、それ以降のテストは実行されません。テストが失敗した時それ以降のテストも失敗する場合、 T.Fatal() を使うべきです

以下ではGoのテスト使用される2つのテクニックを紹介します。

モックとスタブにインターフェースを使用する

外部ライブラリに依存したコードを書いていて、その外部ライブラリが正しく利用されているかテストしたいときを考えます。

Goのインターフェースはメソッドの期待する動作を表しています。 例として io.Writer を見てみます。

type Writer interface {
    Write(p []byte) (n int, err error)
}

io.Writer インターフェースは引数で受け取ったバイト列を書き込みますが、このインターフェースは os.Fileなどで実装されています。Goのtypeシステムではどのインターフェースを使うか明示する必要がありません。既存のtypeのプロパティと一致するインターフェースを宣言することで、外部ライブラリの動作を変更することができます。

例を見ていきましょう。

以下のようなメッセージを送信する外部ライブラリがあります。

type Message struct {
     // ...
}

func (m *Message) Send(email, subject string, body []byte) error {
     // ...
     return nil
}

これをそのまま使うのではなくMessageを使うMessagerインターフェースを作成します。

type Messager interface {
    Send(email, subject string, body []byte) error
}

Alertメソッドでメッセージを送信することを考えます。Message typeを直接渡すのではなくMessager引数で受け取って、インターフェースのSendメソッドを呼び出すようにします。

func Alert(m Messager, problem []byte) error {
    return m.Send("example@example.com", "Critical Error", problem)
}

このようにMessageを抽象化したmessagerを使うことで簡単にモックを作成してテストすることができます。

具体的には以下のようになります。

package msg

import (
    "testing"
)

type MockMessage struct {
    email, subject  string
    body            []byte
}

func (m *MockMessage) Send(email, subject string, body []byte) error {
    m.email = email
    m.subject = subject
    m.body = body
    return nil
}

func TestAleart(t *testing.T) {
    msgr := new(MockMessage) // モックのメッセージを作成します
    body := []byte("Critical Error")

    Alert(msgr, body) // Aleartメソッドを実行します

    if msgr.subject != "Critical Error" {
        t.Errorf("Expected 'critical Error', Got '%s'", msgr.subject)
    }
}

Messagerインターフェースを実装するためにMockMessage typeを作成します。MockeMessageではMessagerと同じSend()が実装されています。このSend()はメーセージを実際に送信するのではなくデータをオブジェクトに保存しておくことでテストしやすくなります。

また、このようにインターフェースを使った抽象化をすることで、後にSend()の動作を変えなければいけなくなった時に簡単に変えられるようになります。

カナリアテスト

外部ライブラリを使っているとメジャーバージョンアップの時などにメソッドの引数が変わることがあります。

例えば、io.Writerを新しく実装していたとします。これをライブラリとして公開していて、他のコードがこれを使用しています。以下のようなコードです。

type MyWriter struct{
     // ...
}

func (m *MyWriter) Write([]byte) error {
     // どこかにデータを書き出す
     return nil
}

ぱっと見io.Writeを実装しているように見えますが、正しくはWrite(p []byte) (n int, err error)です。なのでio.Writeを実装できていません。

次に、type assertionを使ってコードを書いてみます。

func main() {
    m := map[string]interface{}{
        "w": &MyWriter(),
    }
}

func doSomething(m map[string]interface{}) {
    w := m["w"].(io.Writer) // runtime exceptionになる
}

このコードはコンパイルとは通りますが、runtimeでexceptionになります。

これを防ぐために以下のようなカナリアテストを追加します。(ちなみにカナリアテストは”canary in the coal mine”から来ているようです)

func TestWriter(t *testing.T) {
    var _ io.Writer = &MyWriter{} // コンパイラにtype assertionをやってもらう
}

このテストはもちろん失敗します。このようにtype assertionを使ってテストすることで、インターフェースを正しく実装できているか確認することができます。また、外部ライブラリのシグネチャの変更にも気づくことができます。