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を使ってテストすることで、インターフェースを正しく実装できているか確認することができます。また、外部ライブラリのシグネチャの変更にも気づくことができます。