Goのテスト用パッケージを分かりやすく比較してみた

Goのテスト用パッケージを分かりやすく比較してみた


こんにちは、MIIDAS COMPANYの吉田将之です。
Go言語でテストを書く場合、標準でtestingパッケージがあります。
http://golang.jp/pkg/testing

しかしtestingパッケージはassert関数が無かったりと微妙だったので
他のテスト用パッケージを色々比較してみました。

どんなパッケージがあるか

調べてみてざっと以下のパッケージがありました。

比較するパッケージ

全部一から比較するのはさすがにめんどいので、明らかに古いものやメンテナンスがなされてないパッケージは比較対象から外します。
(gospelも実際に使ってみた文献がちらほらあったんですが、2年前からリリース1.0のまま放置されているので比較対象から外すことにします。)

それらを除外した結果、今回比較対象のパッケージは以下とします。
testfy、ginkgo、gocheck、goconvey

それではいざ比較!

主な特徴の比較

testfy ginkgo gocheck goconvey
・直感的で分かりやすい

・テストに必要なのはtestfy/assertパッケージであり、他のtestfy/mockやtestfy/htttpパッケージは独立で使用可能

・BDDスタイルの記法(Rspecに慣れている人は使いやすいかも)

・ファイル保存時にテスト自動実行可能

・テストのスキップができる(Skip関数)

・テストが失敗した時に即時中断もしくは続行するのかを関数で使い分け出来る(AssertとCheck関数)

・ブラウザ上でテスト結果が表示される

・ファイル保存時にテスト自動実行可能

v1.1.3(2016/1/10) v1.2.0(2015/1/1) v1.0 v1.6.1(2016/2/26)
MIT MIT BSD MIT

( ※ 2016年3月時点 )

 

各パッケージ詳細

ここからは実際に各パッケージのテストの書き方・結果出力等について記載します。
モジュールは検証するパッケージごとに分けます。
(testfyパッケージを使用したテストであればtestfy_sample)

testfy

インストール

$ go get github.com/stretchr/testify/assert

テストを書く

単純に等価・非等価・nil判定・true判定のテストを書いてみます。
(比較の為他のパッケージでも同様のテストを書きます)

sample_test.go

package sample
 
import (
	"testing"
	"github.com/stretchr/testify/assert"
)
 
func TestSomething(t *testing.T) {
	// Equal
	assert.Equal(t, 123, 123, "case1:Equal") // Ok
	assert.Equal(t, 123, "123", "case2:Equal") // Fail
 
	// NotEqual
	assert.NotEqual(t, 123, 456, "case3:NotEqual") // Ok
	assert.NotEqual(t, 123, 123, "case4:NotEqual") // Fail
 
	// Nil
	assert.Nil(t, nil, "case5:Nil") // Ok
	assert.Nil(t, 123, "case6:Nil") // Fail
 
	// True
	assert.True(t, true, "case7:True") // Ok
	assert.True(t, false, "case8:True") // Fail
}

テスト実行

$ go test
--- FAIL: TestSomething (0.00s)
	Error Trace:    sample_test.go:11
	Error:		Not equal: 123 (expected)
			        != "123" (actual)
	Messages:	case2:Equal
 
	Error Trace:    sample_test.go:15
	Error:		Should not be: 123
	Messages:	case4:NotEqual
 
	Error Trace:    sample_test.go:19
	Error:		Expected nil, but got: 123
	Messages:	case6:Nil
 
	Error Trace:    sample_test.go:23
	Error:		Should be true
	Messages:	case8:True
 
FAIL
exit status 1
FAIL	testfy_sample/sample	0.076s

ginkgo

インストール

$ go get github.com/onsi/ginkgo/ginkgo
$ go get github.com/onsi/gomega

テストを書く

初期化

ginkgoには初期化コマンドがあり、実行すると親ディレクトリ名にちなんだテスト用ファイルが生成されます
(以下の場合sampleディレクトリ内でコマンド実行しているのでsample_suit_test.goが生成されます)

$ ginkgo bootstrap
Generating ginkgo test suite bootstrap for sample in:
sample_suite_test.go

sample_suite_test.go

package sample_test
 
import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
 
	"testing"
)
 
func TestSomething(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Sample Suite")
}

テスト実装

テストの雛形をginkgo generateコマンドで生成できます

$ ginkgo generate fuga
Generating ginkgo test for Fuga in:
fuga_test.go

fuga_test.go

package sample_test
 
import (
	. "ginkgo_sample/sample"
 
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)
 
var _ = Describe("Fuga", func() {
 
})

生成されたfuga_test.goにテストケースを書きます。

テストを実装したfuga_test.go

package sample_test
 
import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)
 
var _ = Describe("Fuga", func() {
	Describe("Equal", func() {
		// Ok
		It("case1:Equal", func() {
			Expect(123).To(Equal(123))
		})
		// Fail
		It("case2:Equal", func() {
			Expect(123).To(Equal("123"))
		})
	})
	Describe("NotEqual", func() {
		// Ok
		It("case3:NotEqual", func() {
			Expect(123).To(Equal(456))
		})
		// Fail
		It("case4:NotEqual", func() {
			Expect(123).To(Equal(123))
		})
	})
	Describe("Nil", func() {
		// Ok
		It("case5:Nil", func() {
			Expect(nil).To(BeNil())
		})
		// Fail
		It("case6:Nil", func() {
			Expect(123).To(BeNil())
		})
	})
	Describe("True", func() {
		// Ok
		It("case7:True", func() {
			Expect(true).To(BeTrue())
		})
		// Fail
		It("case8:True", func() {
			Expect(false).To(BeTrue())
		})
	})
})

※ importの. "ginkgo_sample/sample"は今回のテストでは不要なので除去しています

テスト実行

go testでも実行できますが、せっかくなのでginkgoコマンドで実行

$ ginkgo
Running Suite: Sample Suite
===========================
Random Seed: 1458443102
Will run 8 of 8 specs
 
•
------------------------------
• Failure [0.001 seconds]
Fuga
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:49
  Equal
  /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:18
    case2:Equal [It]
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:17
 
    Expected
        : 123
    to equal
        : 123
 
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:16
------------------------------
• Failure [0.000 seconds]
Fuga
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:49
  NotEqual
  /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:28
    case3:NotEqual [It]
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:23
 
    Expected
        : 123
    to equal
        : 456
 
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:22
------------------------------
••
------------------------------
• Failure [0.001 seconds]
Fuga
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:49
  Nil
  /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:38
    case6:Nil [It]
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:37
 
    Expected
        : 123
    to be nil
 
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:36
------------------------------
•
------------------------------
• Failure [0.001 seconds]
Fuga
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:49
  True
  /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:48
    case8:True [It]
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:47
 
    Expected
        : false
    to be true
 
    /usr/local/go/src/ginkgo_sample/sample/fuga_test.go:46
------------------------------
 
 
Summarizing 4 Failures:
 
[Fail] Fuga Equal [It] case2:Equal
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:16
 
[Fail] Fuga NotEqual [It] case3:NotEqual
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:22
 
[Fail] Fuga Nil [It] case6:Nil
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:36
 
[Fail] Fuga True [It] case8:True
/usr/local/go/src/ginkgo_sample/sample/fuga_test.go:46
 
Ran 8 of 8 Specs in 0.004 seconds
FAIL! -- 4 Passed | 4 Failed | 0 Pending | 0 Skipped --- FAIL: TestSample (0.00s)
FAIL
 
Ginkgo ran 1 suite in 3.471696811s
Test Suite Failed

エラーの詳細とサマリー(Summarizing以降)を出してくれるみたいですね。
ただなんでこんな単純なテストで3秒もかかるのか・・
テストの速度については後で詳しく検証します。

その他

気になったコマンドについて
ginkgo watch:ファイル保存時にテスト自動実行してくれます
ginkgo -nodes=N:Nに指定した数だけ並列処理でテストを行います

gocheck

インストール

$ go get gopkg.in/check.v1

テストを書く

sample_test.go

package sample
 
import (
	"testing"
	. "gopkg.in/check.v1"
)
 
// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }
 
type MySuite struct{}
 
var _ = Suite(&MySuite{})
 
func (s *MySuite) TestSomething(c *C) {
	// Equal
	c.Check(123, Equals, 123) // Ok
	c.Check(123, Equals, "123") // Fail
 
	// NotEqual
	c.Check(123, Not(Equals), 456) // Ok
	c.Check(123, Not(Equals), 123) // Fail
 
	// Nil
	c.Check(nil, IsNil) // Ok
	c.Check(123, IsNil) // Fail
 
	// True
	// gocheckにはTrue判定用のcheckerがなかったのでEqualsを使用
	c.Check(true, Equals, true) // Ok
	c.Check(false, Equals, true) // Fail
}

テスト実行

$ go test
 
----------------------------------------------------------------------
FAIL: sample_test.go:15: MySuite.TestSomething
 
sample_test.go:18:
    c.Check(123, Equals, "123") // Fail
... obtained int = 123
... expected string = "123"
 
sample_test.go:22:
    c.Check(123, Not(Equals), 123) // Fail
... obtained int = 123
... expected int = 123
 
sample_test.go:26:
    c.Check(123, IsNil) // Fail
... value int = 123
 
sample_test.go:31:
    c.Check(false, Equals, true) // Fail
... obtained bool = false
... expected bool = true
 
OOPS: 0 passed, 1 FAILED
--- FAIL: Test (0.00s)
FAIL
exit status 1
FAIL	gocheck_sample/sample	0.048s

ちなみにCheckではなくAssert関数を使うと、テストが失敗した時点で即時中断されるらしいのでAssertに全て置き換えて再びテスト実行してみる

$ go test
 
----------------------------------------------------------------------
FAIL: sample_test.go:15: MySuite.TestSomething
 
sample_test.go:18:
    c.Assert(123, Equals, "123") // Fail
... obtained int = 123
... expected string = "123"
 
OOPS: 0 passed, 1 FAILED
--- FAIL: Test (0.00s)
FAIL
exit status 1
FAIL	gocheck_sample/sample	0.047s

おー確かに最初のエラーで中断されてますね

その他

テストのスキップ機能がある

goconvey

インストール

$ go get github.com/smartystreets/goconvey

テストを書く

sample_test.go

package sample
 
import (
	"testing"
	. "github.com/smartystreets/goconvey/convey"
)
 
func TestSomething(t *testing.T) {
	// Only pass t into top-level Convey calls
	Convey("simple test", t, func() {
		// Ok
		Convey("case1:Equal", func() {
			So(123, ShouldEqual, 123)
		})
		// Fail
		Convey("case2:Equal", func() {
			So(123, ShouldEqual, "123")
		})
 
		// Ok
		Convey("case3:NotEqual", func() {
			So(123, ShouldNotEqual, 456)
		})
		// Fail
		Convey("case4:NotEqual", func() {
			So(123, ShouldNotEqual, 123)
		})
 
		// Ok
		Convey("case5:Nil", func() {
			So(nil, ShouldBeNil)
		})
		// Fail
		Convey("case6:Nil", func() {
			So(123, ShouldBeNil)
		})
 
		// Ok
		Convey("case7:True", func() {
			So(true, ShouldBeTrue)
		})
		// Fail
		Convey("case8:True", func() {
			So(false, ShouldBeTrue)
		})
	})
}

テスト実行

ブラウザでの実行

goconveyはブラウザ上でテスト結果を表示できるとのこと。
早速試してみましょう。

$ $GOPATH/bin/goconvey

↑コマンドを実行すると自動でブラウザが立ち上がります。

http://127.0.0.1:8080/

立ち上がった時点では画面上部のGoconveyのパスが$GOPATH/binになっているのでこれをテスト対象のディレクトリに書き換えます

すると・・

テストが実行されてエラー詳細が表示されましたね。これは分かりやすぃー

ちなみにテストファイルを修正して保存した時点でもテストが自動実行されるそうなので
テストが通るように修正して保存します。

ブラウザを確認すると自動で実行されているのが分かります。

コマンドラインでの実行

ちなみにgo testコマンドでのテストももちろん可能です。

$ go test
.x.x.x.x
Failures:
 
  * /usr/local/go/src/goconvey_sample/sample/sample_test.go
  Line 17:
  Expected: '123' (string)
  Actual:   '123' (int)
  (Should be equal, type mismatch)
 
  * /usr/local/go/src/goconvey_sample/sample/sample_test.go
  Line 26:
  Expected     '123'
  to NOT equal '123'
  (but it did)!
 
  * /usr/local/go/src/goconvey_sample/sample/sample_test.go
  Line 35:
  Expected: nil
  Actual:   '123'
 
  * /usr/local/go/src/goconvey_sample/sample/sample_test.go
  Line 44:
  Expected: true
  Actual:   false
 
 
8 total assertions
 
--- FAIL: TestSomething (0.00s)
FAIL
exit status 1
FAIL	goconvey_sample/sample	0.043s

テスト速度

上記で記載したテストケースを1万回繰り返して実行した場合(8万件)の速度を各パッケージで比較してみます。

 

testfy ginkgo gocheck goconvey
4.565s 1m15.030663548s 6.693s 30分以上

記載の間違いではありません汗

goconveyが全く終わる気配が無かったので途中でストップしました(_ _)
ginkgoが一番遅いかなーと予想してましたが、goconveyの遅さの次元が違いすぎる・・!
goconveyはテストケース名がユニークでないと怒られる為
以下のようにとりあえずループの中でインデックスをstrconvで文字列に変換してからテストケース名を生成してましたが、それにしてもヤバし。

for i := 0; i < 1000; i++ {
	str := strconv.Itoa(i)
	// Ok
	Convey(str+" case1:Equal", func() {

もしやある程度の数を超えると急激に遅くなるのか?と思い今度は1000回繰り返しの8千件で測定

testfy ginkgo gocheck goconvey
0.489s 9.639207345s 0.997s 416.106s

あ、これでもダメだったorz

テストの書き方を変えてみます。
Conveyでテストケース名を設定する処理を全部取っ払ってみる

		for i := 0; i < 1000; i++ {
			So(123, ShouldEqual, 123)
			So(123, ShouldEqual, "123")
			So(123, ShouldNotEqual, 456)
			So(123, ShouldNotEqual, 123)
			So(nil, ShouldBeNil)
			So(123, ShouldBeNil)
			So(true, ShouldBeTrue)
			So(false, ShouldBeTrue)
		}

テスト実行!

FAIL	goconvey_sample/sample	0.054s

OH・・やはりConveyさんだったか・・!!

ちなみにginkgoも以下のようにシンプルに書いてみたところ約3倍程速くなりました

for i := 0; i < 1000; i++ {
	It("simple test", func() {
		Expect(123).To(Equal(123))
		Expect(123).To(Equal("123"))
		Expect(123).To(Equal(456))
		Expect(123).To(Equal(123))
		Expect(nil).To(BeNil())
		Expect(123).To(BeNil())
		Expect(true).To(BeTrue())
		Expect(false).To(BeTrue())
	})
}

テスト速度

Ginkgo ran 1 suite in 3.428716611s

ginkgoとgoconveyは書き方によってテスト速度にかなり影響しますね。
そしてtestfyが一番速いことが分かりました。

その他の比較

各パッケージの細かな機能の違いについては以下ページに記載してあります。
https://github.com/shageman/gotestit

最後に

現時点ではどれが圧倒的に良いというわけではなく、用途によって何のパッケージを選択するかは変わってくると思います。
例えばシンプルでかつ速さを求めるならtestfy、画面でテスト結果を表示したいのならgoconveyといったように。

MIIDASでは速度を重視して現在testfyを導入しています。

この記事が皆さんにとってGoのテスト導入の際に役立ててれば幸いです。