TL;DR
- ginkgo(Go言語のビヘイビア駆動開発用のテストフレームワーク)で,golangのテストを実行する方法を紹介します
- 以下の内容について記載しています
- ginkgo, gomegaのインストール
- チュートリアル
- テストの雛形の作成手順
- 実装
- テストの実行(Pass時,Fail時)
Ginkgo概要
-
Ginkgoは,Go言語のビヘイビア駆動開発用のテストフレームワークです
- 複雑なテストを記述しやすい,と言っています
- Ginkgoは,GomegaというMather(テストの条件マッチ)と組み合わせて使います
- assertはカッコよくないらしいので,Ginkgoなどでコードの振る舞いをベースにテストを記述します
GinkgoとGomegaのインストール
-
Ginkgoの案内にしたがって,ライブラリをインストールします
- ginkgoはコマンドの形で提供されます
-
go test
の代わりに実行します - それ以外にも,テスト用のテンプレートを作る機能もあります
# 環境変数GOPATHで指定した場所のbinディレクトリにginkgoがインストールされます
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...
which ginkgo
~/.go/bin/ginkgo
チュートリアル
テスト付きのパッケージ作成
-
テスト付きパッケージは,以下の手順で作ります
- 既存のパッケージファイルにginkgo用のテストファイルを設置することもできますが,テストファーストの考え方から,テストから作ります
- パッケージのテストスイートファイルを作る
- パッケージファイルごとのテストファイルを作る
-
テストスイートファイル,テストファイルは,作成のためのコマンドが提供されています
- パッケージを格納するディレクトリ(この記事の例ではPerson)を作成して,
ginkgo bootstrap
コマンドを実行すると,Personパッケージ用のテストスイートファイルPerson_suite_test.go
が作成されます - Personパッケージのテストを行うための共通設定です
- パッケージを格納するディレクトリ(この記事の例ではPerson)を作成して,
# Aliceパッケージを作るディレクトリを作成する
mkdir Person && cd Person
# テストスイートファイルの作成
ginkgo bootstrap
# Generating ginkgo test suite bootstrap for Person in:
# Person_suite_test.go
# テストファイルの作成
ginkgo generate Person
# Generating ginkgo test for Person in:
# Person_test.go
- テストスイート,テストファイルを作っただけでは,パッケージのテストができませんので,パッケージ本体(この記事ではPerson)を作ります
パッケージPersonの確認
- 以下のようなパッケージPersonを作ることを考えます
- 構造体Personを持つ
- 構造体Personは,string型のフィールドnameを持つ
- フィールドnameに関するSetterとGetterである以下のメソッドを持つ
-
SetName(string)
: フィールドnameをセットする -
GetName() (string, error)
: nameを読み出す.nameがセットされていない場合はエラー"Name is not set"が返る
-
- テストファーストの考え方から,テストからまず作ります
Person_test.goの実装
-
ginkgo generate
で作られたPerson_test.goは雛形で,中身がありません - そのため,前節のPersonの仕様を満たすようにテストを記述します
- 2つのメソッドに対し,3つのテストを行っています
- SetNameするだけのテスト
- SetNameしたnameがGetNameで取り出せることを確認する
- GetNameでエラーが返る(nameをセットする前にGetNameする)パターンを確認する
- Ginkgoの書き方をしています.Gomegaで変数のマッチ処理を行います
- 2つのメソッドに対し,3つのテストを行っています
/*
Test file for Person
Person_test.go
*/
package Person_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"errors"
. "." // Person.goと同じディレクトリで実行するので,カレントディレクトリをimportします
)
var _ = Describe("Person", func() { // Personに関するテストです.文字列は変更できます
Context("Test for Name", func() { // Nameに関するテストです.文字列は変更できます
/* Person.SetNameメソッドのテスト */
It("Test for SetName", func() {
p := &Person{} // 構造体Personを作ります
// エラーは発生しないので特段の検証は必要でない
p.SetName("Alice") // SetNameメソッドを実行します
})
/* Person.GetNameメソッドのテスト(正常系) */
It("Test for GetName (Normal)", func() {
p := &Person{}
p.SetName("Bob") // SetNameメソッドでnameにBobをセットします
name, err := p.GetName() // GetNameメソッドでnameを読み出します
Expect(err).To(BeNil()) // エラーは発生しない(nil)ことが期待されます
Expect(name).To(Equal("Bob")) // nameはBobだと期待されます
})
/* Person.GetNameメソッドのテスト(エラー系) */
It("Test for GetName (Error)", func() {
p := &Person{}
_, err := p.GetName() // エラーなはずなので,nameは使用しません
// nameが設定されていないので,エラーが返る
Expect(err).NotTo(BeNil()) // エラーが発生します(err != nil)
Expect(err).To(Equal(errors.New("Name is not set"))) // エラー内容が正しいか確認します
})
})
})
-
Describe
,Context
,It
の順で,階層的なブロックに分けてテストを記述します - 各ブロックの中で,テストを記述します
- この記事の場合は,構造体Personとその関連メソッドSetName, GetNameに関するテストです
- メソッドを実行して,返り値として期待される値を,
Expect
メソッドを使って記述します-
Expect
メソッドの引数に判定したい変数を指定します - 続いて,True(
To
)またはFalse(NotTo
)を判定するメソッドを設定します -
To
,NotTo
メソッドの引数として,値を設定します
-
Person.goの実装
- 仕様に基づいて,
Person.go
を実装します
/*
Person.go
*/
package Person
import "errors"
type Person struct{
name string
}
func (p *Person) GetName() (string, error) {
if p.name == "" {
return "", errors.New("Name is not set")
}
return p.name, nil
}
func (p *Person) SetName(in_name string) {
p.name = in_name
}
ginkgoでテストの実行(すべてのテストがパスするとき)
-
Person_test.go
,Person.go
が実装できたので,テストを実行します-
Person_suite_test.go
はこの記事の段階で編集する必要はありません
-
- テストの実行は,
ginkgo
コマンドまたはgo test
コマンドです- 以下の例では,カバレッジレポートも作っています
# ginkgoの代わりにgo testコマンドでも良い
ginkgo -v -cover -coverprofile=./cover.out
Running Suite: Person Suite
===========================
Random Seed: 1557386542
Will run 3 of 3 specs
Person Test for Name
Test for SetName
/Person/Person_test.go:14
•
------------------------------
Person Test for Name
Test for GetName (Normal)
Person/Person_test.go:20
•
------------------------------
Person Test for Name
Test for GetName (Error)
/Person/Person_test.go:28
•
Ran 3 of 3 Specs in 0.000 seconds
SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
coverage: 100.0% of statements
Ginkgo ran 1 suite in 1.159211242s
Test Suite Passed
- 各テストが通過した場合は,PASSが表示されます
ginkgoでテストの実行(テストの一部がパスしなかったとき)
- もしもテストに失敗した場合は,失敗した部分でその旨が出力されます
- 以下の正常系GetNameのテストで,
err
にnilでない値が入っていると記述した場合は,テストがパスしません
- 以下の正常系GetNameのテストで,
// Person.go
func (p *Person) GetName() (string, error) {
if p.name == "" {
return "", errors.New("Name is not set")
}
return p.name, errors.New("No Error") // 返り値にnilでない値を設定している
// return p.name, nil
}
- ginkgoでテストを実行すると,以下のようにFAILしたテストと関連情報が出力されます
$ ginkgo -v -cover -coverprofile=./cover.out
Running Suite: Person Suite
===========================
Random Seed: 1557387339
Will run 3 of 3 specs
Person Test for Name
Test for SetName
/Person/Person_test.go:14
•
------------------------------
Person Test for Name
Test for GetName (Normal)
/Person/Person_test.go:20
• Failure [0.000 seconds]
Person
/Person/Person_test.go:11
Test for Name
/Person/Person_test.go:12
Test for GetName (Normal) [It]
/Person/Person_test.go:20
Expected
<*errors.errorString | 0xc0001b6150>: {s: "No Error"}
to be nil
/Person/Person_test.go:24
------------------------------
Person Test for Name
Test for GetName (Error)
/Person/Person_test.go:28
•
Summarizing 1 Failure:
[Fail] Person Test for Name [It] Test for GetName (Normal)
/Person/Person_test.go:24
Ran 3 of 3 Specs in 0.001 seconds
FAIL! -- 2 Passed | 1 Failed | 0 Pending | 0 Skipped
--- FAIL: TestPerson (0.00s)
FAIL
coverage: 100.0% of statements
Ginkgo ran 1 suite in 1.2348396s
Test Suite Failed
-
Expect
で記述した期待の変数値と,実際の変数値が表示されているので,デバッグに利用できます
まとめ
- ginkgoフレームワークを用いると,「振る舞い(関数の入出力関係)」ベースでテストを記述することができます
- プログラムコードもテストに合わせて実行されるので,きれいなコードになる傾向があります
- 今後の展開
- モックとの組み合わせ
- ランダムな数値を入力とする場合の対応