基本的なユニットテスト


ユニットテストの書き方

*_test.goの作成

Go言語は、ソースコードのファイル名で製品コードとテストコードを見分けます。 テストコードには、_test.goで終わるファイル名を使用します。

例
main_test.go
data_import_test.go

testingパッケージのインポート

テストコードの冒頭では、testingパッケージをインポートします。

import "testing"

testingパッケージには、テスト実行に役立つ以下の構造体が含まれています。

testing.T構造体
テストを失敗させたり、テストメッセージの出力をするために使用します
testing.M構造体
テスト実行のメインルーチンを明示的に作成する際に使用します。(本項では触れません)
testing.B構造体
ベンチマークテストをする際に使用します。(本項では触れません)

テストメソッドの作成

テストメソッドは、以下のような形式で作成します。

// Xxxの部分は自由に決めてよい
func TestXxx(t *testing.T) {
    // ...
}

テストメソッド内では、主に引数tの以下のメンバを用いてアサーションを行います。

t.Error(出力メッセージ1, 出力メッセージ2, ・・・)
t.Errorf(フォーマット指定子, 出力パラメータ1, 出力パラメータ2, ・・・)

上記二つのメソッドが呼ばれると、呼び出し元のテストメソッドは失敗したものと扱われます。 また、引数に指定したメッセージを失敗メッセージとして出力します。

ErrorとErrorfの違いは、前者がすべての引数をスペース区切りで出力するのに対し、 Errorfは第一引数に指定したフォーマット指定子中に第二引数以降のパラメータを挿入して出力します。 (fmt.Printlnとfmt.Printfの関係に似ていますが、Errorfは自動的に末尾に改行を出力する点がfmt.Printfと異なります)

t.Fatal(出力メッセージ1, 出力メッセージ2, ・・・)
t.Fatalf(フォーマット指定子, 出力パラメータ1, 出力パラメータ2, ・・・)

上記二つのメソッドもError/Errorfと同様、メッセージを出力してテストを失敗させます。 Error系との違いは、Fatal系が呼び出し元のテストメソッドの実行を即座に終了させるのに対し、 Error系はテストを失敗扱いにするものの、処理はそのまま継続させます。

t.Log(出力メッセージ1, 出力メッセージ2, ・・・)
t.Logf(フォーマット指定子, 出力パラメータ1, 出力パラメータ2, ・・・)

上記二つのメソッドはテストメッセージの出力に利用します。 Error系やFatal系と違い、これらのメソッドはテストを失敗させません。

デバッグ用のメッセージ出力や、失敗メッセージが複数行に渡る場合などに使用します。

テストメソッドの例

例えば、以下の様な製品コードがあったとします。

package cart

type Cart struct {
    products []string
}

func New() *Cart {
    c := new(Cart)
    c.products = make([]string, 0)
    return c
}

func (c *Cart) Add(s string) {
    c.products = append(c.products, s)
}

func (c *Cart) GetAll() []string {
    return c.products
}

商品名だけを追加していく、非常に簡略化されたショッピングカートです。 これに対してテストコードを書いてみましょう。

package cart

import "testing"

func TestAddAndGetProductsInCart(t *testing.T) {
    c := New()
    c.Add("りんご")
    c.Add("みかん")

    products := c.GetAll()
    if len(products) != 2 {
        t.Fatalf("商品の数が想定と違う。(商品数:%d)", len(products))
    }
    if products[0] != "りんご" && products[1] != "りんご" {
        t.Error("りんごがカートに入っていない。")
        t.Log("カートの中身:", products)
    }
    if products[0] != "みかん" && products[1] != "みかん" {
        t.Error("みかんがカートに入っていない。")
        t.Log("カートの中身:", products)
    }
}

カートにりんごとみかんを入れて、取得ができるか確かめるテストコードを書いてみました。

注意深く見てみると、カートの中身が2個でない場合は(特に2個に満たない場合に)以降のコードで参照エラーを発生させる可能性があるため、 カート内の商品数チェックにはFatal系のメソッドを使用し、条件を満たさないばあはすぐにテストメソッドを抜けるようにしています。

逆に、りんごやみかんといった個々の商品が入っているかのチェックはそれぞれ排他に行う必要がないため、 Error系のメソッドを使用してすべてのチェックを通るようにしているのがわかります。

Go言語ではJava等の言語にあるassert~系のメソッドが無く、自分で書いた条件式とメッセージングのみでテストを行います。 面倒に見えますが、開発者が自分でメッセージを書くことで よりわかりやすいテストメッセージを書くよう心掛けるようになるというのが公式見解です。

テストの実行

テストを実行するには、以下のコマンドを発行します。(パッケージディレクトリ名はimport文で指定する物と同一です)

go test パッケージディレクトリ名

例えば、先述のテストコードを実行してみましょう。

$ go test cart
ok      cart    0.104s

見事、テストの実行に成功しました。

引き続き、失敗例も見てみましょう。Addメソッドの中身を空にしてテストを実行してみます。

$ go test cart
--- FAIL: TestAddAndGetProductsInCart (0.00s)
        cart_test.go:12: 商品の数が想定と違う。(商品数:0)
FAIL
FAIL    cart    0.102s

テストが失敗したことと、その原因が一目で見て判ります。