sgykfjsm.github.com

Ginkgo 基本的な使い方編

最近、golangでプログラムを書く機会が増えてきた。golangでTDDをする場合、標準のtestingパッケージを使うのが一般的なようだ。ただし、標準パッケージだけだとちょっとテストが書きづらいので、stretchr/testifyを使っている人も多いと思う。

関数のテストをしたいときは標準パッケージなりtestifyを使うなりで良いのだけど、振る舞いをテストしたい、つまりBDDをしたいなーと思った時にちょっと調べらたらGinkgoというのが良さ気だったので、ちょっと試してみる。

Ginkgoとは

表現力があって包括的なテストを効率良く書くためのBDDスタイルのテストフレームワーク。GomegaというMatcherライブラリと併用すると良い感じらしいけど、単体でも充分使えるらしい。

今回はドキュメントに従い、GinkgoだけでなくGomegaもインストールした。

インストール

go getするだけ。ginkgoの実行バイナリが${GOPATH}/binに、ソースコードはgomegaとともに$GOPATH/src/github.com/onsiにインストールされていることが確認できる。

1
2
3
4
5
6
7
8
9
$ go get github.com/onsi/ginkgo/ginkgo
$ go get github.com/onsi/gomega
$ ls -l $GOPATH/bin
...
-rwxr-xr-x   1 sgyk  staff  11747788  9 24 23:00 ginkgo*
$ ls -l $GOPATH/src/github.com/onsi
...
drwxr-xr-x  16 sgyk  staff  544  9 24 23:00 ginkgo/
drwxr-xr-x  17 sgyk  staff  578  9 24 23:01 gomega/

始め方

こんなかんじでginkgo bootstrapをすると、テストスイートを生成してくれる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ mkdir ginkgo-study
$ cd $_
$ ginkgo bootstrap
Generating ginkgo test suite bootstrap for ginkgo_study in:
        ginkgo_study_suite_test.go
$ cat ginkgo_study_suite_test.go
package ginkgo_study_test

import (
        . "github.com/onsi/ginkgo"
        . "github.com/onsi/gomega"

        "testing"
)

func TestGinkgoStudy(t *testing.T) {
        RegisterFailHandler(Fail)
        RunSpecs(t, "GinkgoStudy Suite")
}

自動生成されたファイルはginkgoでテストを走らせることができるし、Ginkgoは標準パッケージであるtestingにフックしているので、go testで実行させることも出来る。

1
2
3
4
5
6
7
8
9
10
11
12
$ ginkgo
Running Suite: GinkgoStudy Suite
================================
Random Seed: 1443106503
Will run 0 of 0 specs


Ran 0 of 0 Specs in 0.000 seconds
SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped PASS

Ginkgo ran 1 suite in 3.234616229s
Test Suite Passed
1
2
3
4
5
6
7
8
9
10
$ go test
Running Suite: GinkgoStudy Suite
================================
Random Seed: 1443106514
Will run 0 of 0 specs


Ran 0 of 0 Specs in 0.000 seconds
SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped PASS
ok      github.com/sgykfjsm/ginkgo-study        0.077s

自動生成されたテストスイートの確認

まずは1行目。

1
package ginkgo_study_test

このパッケージ名はginkgo bootstrapが実行されたディレクトリ名を元に設定されている。今回はginkgo-studyというディレクトリの中で実行したので、ginkgo_study_testとなっている。もちろん敢えてginkgo_studyなど、別名に修正することは可能だが、テストコードと実コードとの切り分けの観点からすると、このままで良いだろう。

続いて3行目から8行目について。

1
2
3
4
5
6
import (
        . "github.com/onsi/ginkgo"
        . "github.com/onsi/gomega"

        "testing"
)

ドットインポートを使うことでginkgoとgomegaをtop levelでインポート、つまり修飾子なしで各パッケージの関数を使えるようにしている。詳しくはSOのWhat does the ‘.’ (dot or period) in a go import statement do?の回答と公式ドキュメントのImport declarationsを参照のこと。

ドットインポートをしたくない場合は、bootstrap実行時にオプション--nodotをつけることで回避できる。

では最後に自動生成されたテスト関数を確認する。

1
2
3
4
func TestGinkgoStudy(t *testing.T) {
        RegisterFailHandler(Fail)
        RunSpecs(t, "GinkgoStudy Suite")
}

TestGinkgoStudyはもちろんただの関数名。引数にt *testing.Tがあることからわかるようにgo testでこの関数を実行することが出来る。

11行目のRegisterFailHandlerは、GinkgoのFail関数を引数にすることで、テストが失敗した際にGomegaへGinkgoのFail関数を渡している。このRegisterFailHandlerはGinkgoとGomegaの間の唯一の接点となっている。

12行目のRunSpecでテストのスペック、つまりテストの内容を設定する。

ドキュメントに沿って細かく見ていったけど、全体的に難しいことはあまり無いように見える。Ginkgo(に限らずBDD全般がそうだと思うけど)ではテストをどう実装するか、よりもどのようにテストを設計するかに注力しやすくなっている。

テストスペックを作る

テストスペックはテストスイートと同様にコマンドで自動生成出来る。と言っても、雛形だけど。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ginkgo generate ginkgo_study
Generating ginkgo test for GinkgoStudy in:
  ginkgo_study_test.go

$ cat ginkgo_study_test.go
package ginkgo_study_test

import (
        . "github.com/sgykfjsm/ginkgo-study"

        . "github.com/onsi/ginkgo"
        . "github.com/onsi/gomega"
)

var _ = Describe("GinkgoStudy", func() {

})

ginkgo generateにパッケージ名を渡せば良い。

自動生成されたテストスペックの確認

1行目については特に言うことはないと思うけど、ginkgo generateの引数が設定される。

1
package ginkgo_study_test

3行目から7行目にかけてはテストスイートと違いがある。1つはテスト対象のパッケージが設定されていること、もう1つはtestingパッケージがimportされていないこと。前者については特に疑問はないと思うけど、後者についてはちょっと違和感があるかもしれない。というのも、一般的にgolangを使ったプログラム開発では、テストコードはxxx_test.goというネーミングが推奨されており、そのファイル内ではtestingパッケージをimportすることがほとんどだが、これはそういった慣習と異なっている。まぁ慣れの問題かもしれないが、ginkgoを使ったテストコードだということを知らないと違和感があると思う。

1
2
3
4
5
6
import (
        . "github.com/sgykfjsm/ginkgo-study"

        . "github.com/onsi/ginkgo"
        . "github.com/onsi/gomega"
)

以降のコードは”コンテナ”と捉えることができ、スペックを記述(=格納)するクロージャとなる。開発者はこのコンテナの中にスペックを記述することになる。

1
2
3
var _ = Describe("GinkgoStudy", func() {

})

ところで、var _ = ...という記述は今回初めて見かけた。ドキュメントによると、var _ = ...と書くことで、func init(){}無しにトップレベルで(つまりは初期処理として)Describeが評価されることができるらしい。

テストスペックを書く

では実際にテストスペックを書いてみる。今回は以下の様なモデルと、その振る舞いを書いた。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package ginkgo_study

type Grade struct {
  Subject   string
  Score     int
  Mandatory bool
}

func (g *Grade) IsPass() bool {
  if g.Score > 60 {
      return true
  }
  return false
}

…まぁこれについては特にいうことは無いと思う。つぎに、対応するスペックを以下のようにした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package ginkgo_study_test

import (
  . "github.com/sgykfjsm/ginkgo-study"

  . "github.com/onsi/ginkgo"
  . "github.com/onsi/gomega"
)

var _ = Describe("GinkgoStudy", func() {

  var (
      goodGrade Grade
      badGrade  Grade
  )

  BeforeEach(func() {
      goodGrade = Grade{
          Subject:   "Math",
          Score:     61,
          Mandatory: true,
      }

      badGrade = Grade{
          Subject:   "History",
          Score:     60,
          Mandatory: false,
      }
  })

  Describe("Criteria of pass or not", func() {
      Context("With more than 60", func() {
          It("should be passed", func() {
              Expect(goodGrade.IsPass()).To(Equal(true))
          })
      })

      Context("With less than 60", func() {
          It("should be failed", func() {
              Expect(badGrade.IsPass()).To(Equal(false))
          })
      })
  })

})

テストスペックの解説

先にも説明したように、開発者はDescribeのクロージャで表現されたコンテナの中にスペックを書けば良い。

BeforeEachはいわゆるsetupにあたる処理。スペックが実行される度に、BeforeEachで定義した”状態”が作られる。また、ここで定義した”状態”は後述するItの中で使うことが出来る。

DescribeContextを使って、スペックの内容を表現豊かに記述することができ、Itでスペックを指定する。BeforeEachItで”状態”を共有するためには、コンテナの中のトップレベルで変数を定義すれば良い。

Itの中で使っているExpectはGomega由来のもの。これを使って評価したい内容を記述すれば良い。

テストスペックを実行する。

このテストスペックを実行してみる。実行方法は先述の通り、普通にgo testすれば良い。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ go test -v -cover
=== RUN TestGinkgoStudy
Running Suite: GinkgoStudy Suite
================================
Random Seed: 1443114232
Will run 2 of 2 specs

••
Ran 2 of 2 Specs in 0.000 seconds
SUCCESS! -- 2 Passed | 0 Failed | 0 Pending | 0 Skipped --- PASS: TestGinkgoStudy (0.00 seconds)
PASS
coverage: 100.0% of statements
ok      github.com/sgykfjsm/ginkgo-study        0.073s

見ての通り、-coverで同時にカバレッジを出すことができる。また、テストに失敗すると、以下の様になる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ go test -v -cover
=== RUN TestGinkgoStudy
Running Suite: GinkgoStudy Suite
================================
Random Seed: 1443114347
Will run 2 of 2 specs

• Failure [0.004 seconds]
GinkgoStudy
/Users/sgyk/local/script/golang/src/github.com/sgykfjsm/ginkgo-study/ginkgo_study_test.go:45
  Criteria of pass or not
  /Users/sgyk/local/script/golang/src/github.com/sgykfjsm/ginkgo-study/ginkgo_study_test.go:43
    With more than 60
    /Users/sgyk/local/script/golang/src/github.com/sgykfjsm/ginkgo-study/ginkgo_study_test.go:36
      should be passed [It]
      /Users/sgyk/local/script/golang/src/github.com/sgykfjsm/ginkgo-study/ginkgo_study_test.go:35

      Expected
          <bool>: false
      to equal
          <bool>: true

      /Users/sgyk/local/script/golang/src/github.com/sgykfjsm/ginkgo-study/ginkgo_study_test.go:34
------------------------------

Summarizing 1 Failure:

[Fail] GinkgoStudy Criteria of pass or not With more than 60 [It] should be passed
/Users/sgyk/local/script/golang/src/github.com/sgykfjsm/ginkgo-study/ginkgo_study_test.go:34

Ran 2 of 2 Specs in 0.004 seconds
FAIL! -- 1 Passed | 1 Failed | 0 Pending | 0 Skipped --- FAIL: TestGinkgoStudy (0.00 seconds)
FAIL
exit status 1
FAIL    github.com/sgykfjsm/ginkgo-study        0.071s

見ての通り、かなり丁寧に失敗箇所を教えてくれる。ちなみに、Ginkgoの出力は色付きであり、一部はグレーで表示されるので、ここで見るほど冗長には感じないものと思われる。

まとめ

以上、ごくごく簡単にGinkgoの使い方を示した。といっても、内容的には公式ドキュメントの冒頭をなぞっただけだが…。

ただ、見ての通り、かなり使いやすいことがわかると思う。コマンドによるボイラープレートや雛形の生成により、開発者は集中すべきことだけに集中できるし、他のBDDフレームワークと同様に自然言語に近い感覚でテストを書くことができる。

また、テスト失敗時に細かい出力をしてくれるのも地味に嬉しい。標準のtestingパッケージやtestifyなどはシンプルな出力しかしてくれないのでデバッグコードが必要になる時があるが、Ginkgoだとその必要はだいぶ減らすことができるだろう。とはいえ、やや丁寧すぎる気がしないでもないが。

個人的な感覚としてはGinkgoはとても開発者フレンドリーだし、比較的活発に開発されているので、注目すべきライブラリだと思う。