Ginkgoでgolangのビヘイビア駆動(BDD)開発入門

# TL;DR

* ginkgo(Go言語のビヘイビア駆動開発用のテストフレームワーク)で，golangのテストを実行する方法を紹介します
* 以下の内容について記載しています
    * ginkgo, gomegaのインストール
    * チュートリアル
        * テストの雛形の作成手順
        * 実装
        * テストの実行(Pass時，Fail時)

# Ginkgo概要

* [Ginkgo](https://onsi.github.io/ginkgo/)は，Go言語のビヘイビア駆動開発用のテストフレームワークです
    * 複雑なテストを記述しやすい，と言っています
    * Ginkgoは，[Gomega](http://onsi.github.io/gomega/)というMather(テストの条件マッチ)と組み合わせて使います
    * assertはカッコよくないらしいので，Ginkgoなどでコードの振る舞いをベースにテストを記述します

# GinkgoとGomegaのインストール

* [Ginkgo](https://onsi.github.io/ginkgo/)の案内にしたがって，ライブラリをインストールします
    * ginkgoはコマンドの形で提供されます
    * ```go test```の代わりに実行します
    * それ以外にも，テスト用のテンプレートを作る機能もあります

```bash
# 環境変数GOPATHで指定した場所のbinディレクトリにginkgoがインストールされます
go get github.com/onsi/ginkgo/ginkgo
go get github.com/onsi/gomega/...

which ginkgo
~/.go/bin/ginkgo
```

# チュートリアル

## テスト付きのパッケージ作成

* テスト付きパッケージは，以下の手順で作ります
    * 既存のパッケージファイルにginkgo用のテストファイルを設置することもできますが，テストファーストの考え方から，テストから作ります
    1. パッケージのテストスイートファイルを作る
    1. パッケージファイルごとのテストファイルを作る

* テストスイートファイル，テストファイルは，作成のためのコマンドが提供されています
    * パッケージを格納するディレクトリ（この記事の例ではPerson）を作成して，```ginkgo bootstrap```コマンドを実行すると，Personパッケージ用のテストスイートファイル```Person_suite_test.go```が作成されます
    * Personパッケージのテストを行うための共通設定です

```bash
# 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で変数のマッチ処理を行います

```go
/*
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```を実装します

```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```コマンドです
    * 以下の例では，カバレッジレポートも作っています

```bash
# 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でない値が入っていると記述した場合は，テストがパスしません

```go
// 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フレームワークを用いると，「振る舞い（関数の入出力関係）」ベースでテストを記述することができます
* プログラムコードもテストに合わせて実行されるので，きれいなコードになる傾向があります
* 今後の展開
    * モックとの組み合わせ
    * ランダムな数値を入力とする場合の対応

# 参考

* [Ginkgo](https://onsi.github.io/ginkgo/)
* [Gomega](http://onsi.github.io/gomega/)