この記事は BrainPad Advent Calendar 2020 9日目の記事になります
会社ではピ●チュウで通っております。
よろしくお願いします。
筆者視点(APIサーバー視点)な成分が多くありますが、testing framework の比較記事となります。
結論と結論までの流れ
結論: go testing サイコー!!
公式FAQより:Where is my favorite helper function for testing?
A related point is that testing frameworks tend to develop into mini-languages of their own, with conditionals and controls and printing mechanisms, but Go already has all those capabilities; why recreate them? We'd rather write tests in Go; it's one fewer language to learn and the approach keeps the tests straightforward and easy to understand.
(意訳)testing framework のトレンドは、"条件、制御、出力の機構をいかに楽にするか?"
って話なら golang ならすでに内包してあるよ。
↪︎ 確かに!!testing frameworks 使う必要なかったです。と帰結したお話になります
流れ: 以下の記事内容になります
- test framework をみる観点を説明する
- 各観点で評価してみる
- まとめて、結論につなげる
余談:
fmt_test.go を読むと勉強になるらしい
The standard Go library is full of illustrative examples, such as in the formatting tests for the fmt package.
👨🎓 testing framework対象と観点の説明
各testing framework について、以下の観点で評価していきます。
- testing package/framework
- testing ... 標準パッケージ
- httpexpect ... http 関連のもの
- goconvey ... テストの補助機能多め
- ginkgo, gomega ... BDD testing framework
- 観点
- (1) 単体テスト ... testing framework に依存しない事を話す
- (2) BDD
- BDDの重要ポイント ... wikipedia ベースの観点
- Test のX性観点と記述面
- Testability(テスト容易性)/Understandability(理解容易性)/Modifiability(変更容易性)
- 記述面
- assertion ... assert 標準以外
- CIでのReporting ... レポートする
- (3) 受け入れテスト(AcceptanceTest) ... ポエムが書いてあります
(1) unittest
- TableDrivenTests で unittest はおおよそカバーできる。(testing framework不要)
- 対象となる testing framework は unittest の効率化・品質向上に繋がりにくいと判断して評価してません。
- testify/assert があると便利だと思います
- ※ いろんなところで記事があるので、testing でのtest内容は割愛します
ginkgo/goconvey/testing sample code
📝 ginkgo
Context("logic test ===> unit test", func() {
It("Hello unittest", func() {
Expect(app.Hello(testContext)).To(Equal("Hello"))
})
})
📝 goconvey
Convey("logic test ===> unit test", func() {
Convey("Hello unittest", func() {
So(app.Hello(ctx), ShouldEqual, "Hello")
})
})
📝 testing
t.Run("logic test ===> unittest", func(t *testing.T) {
if app.Hello(ctx) != "Hello" {
t.Errorf("Hello not match")
}
})
unitest 小ネタ
・RDB のテスト
弊社では、go-sqlite3を利用してRDBの簡易テスト可能にしています、必要なテーブルとモデルとデータをセットにできナレッジの共有面で良いと思っています。
擬似コード
import "memdb" // 自作パッケージ
func TestUseRDB(t *testing.T) {
cfg := memdb.Config{
// model ベースでTableを作成
Tables: map[string]interface{}{"test_table": model.TestTable{}},
// テストに必要なデータを用意
ExecSQL: `INSERT INTO test_table (id, text) values (1, 'hello');`,
// SQLの標準出力
SQLTrace: true,
}
dberr := memdb.Run(context.Background(), cfg, func(c context.Context, tx sql.Transaction) error {
// do something
})
}
・Http Server を起動しないTest
サーバーの起動は環境整備コストが高いため、このようなアプローチもとっています。
import (
"testing"
"net/http/httptest"
)
func TestEndpoint(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, serverURL + "/", nil)
w := httptest.NewRecorder()
testHandler.ServeHTTP(w, req)
res := w.Result()
if res.StatusCode != http.Statusxxx {
t.Errorf("xxxx")
}
body, err := ioutil.ReadAll(res.Body)
if "Expect Value" != string(body) {
t.Errorf("xxxxx")
}
}
(2) BDD
BDDの重要ポイントと参考資料(P.26,27)での3観点のX性を支える4観点+コード記述感で比較します。
- BDD重要ポイント (wikiより)
- 何処から始まるか?理解可能にする(Where to start in the process)
- テストが必要・不必要の判断を可能にする(What to test and what not to test)
- 一つにどれだけテストが必要か理解可能にする(How much to test in one go)
- テストと呼ぶものは何かを明確にする(What to call the tests)
- テストを失敗した理由を理解可能にする(How to understand why a test fails)
- Testability(テスト容易性)/Understandability(理解容易性)/Modifiability(変更容易性)
- Operability ... 実行円滑性(容易かつ高速な実行が良い)
- Observability ... 観測容易性(結果がわかり易く、その後工程に移りやすいと良い)
- Controllability ... 制御容易性(準備、対象操作、後処理などし易いと良い)
- Decomposability ... 分解容易性(分割、結合、置換が容易だと良い)
BDDの重要ポイントの比較
対象のtesting framework では以下のような構造で記述が出来ます。
この形式を用いて、BDDの重要ポイントの表現や実現性などが評価点となりますが、
BDD重要ポイントを満たすためには、Test設計/記述者の責務になり、testing framework の選択に依存した効果は無いと判断しています。
func Testxxx() {
Hoge("Test description", func() {
Fuga("Test description", func() {
// do something
})
})
}
※ httpexpect
は除きます
Testの X 性と記述面の比較
goconvey が良さそうに見えますが、httpに通信結果のAssertionでは httpexpect の圧勝です。
観点 | ginkgo,gomega | goconvey | httpexpect | testing |
---|---|---|---|---|
Operability | △ ginkoの作法がある |
◯ | ◯ | ◯ |
Observability | △(簡素すぎる) | ◎(web ui有) | ◯ | ◯ |
Controllability | ◯ | ◯ | ◯ | ◯ |
Decomposability | ◯ | ◯ | ◯ | ◯ |
Assertion | △ | △(書き心地は良) | ◯ | x |
Reporting | △(実装が必要) | ◯ | ◯ | ◯ |
(補足)assertion
ginko, convey ともに http の Body のassert するには不便な所がある
httpexpect
を利用したい
assertのコード
ginkgo
Context("api test ===> e2e test", func() {
It("GET", func() {
res, err := http.Get(serverURL + "/")
Expect(err).ShouldNot(HaveOccurred())
Expect(res.StatusCode).Should(Equal(http.StatusOK))
body, err := ioutil.ReadAll(res.Body)
Expect(err).ShouldNot(HaveOccurred())
_ = res.Body.Close()
Expect(string(body)).To(Equal("Hello"))
})
It("POST", func() {
res, err := http.Post(serverURL+"/", "application/json", strings.NewReader("{}"))
Expect(err).ShouldNot(HaveOccurred())
Expect(res.StatusCode).Should(Equal(http.StatusOK))
body, err := ioutil.ReadAll(res.Body)
Expect(err).ShouldNot(HaveOccurred())
_ = res.Body.Close()
Expect(string(body)).To(Equal(`{"value":"Hello"}`))
})
})
convey
Convey("API Test", func() {
Convey("GET", func() {
res, err := http.Get(serverURL + "/")
defer func() { _ = res.Body.Close() }()
So(err, ShouldBeNil)
So(res.StatusCode, ShouldEqual, http.StatusOK)
text, err := readBody(res)
So(err, ShouldBeNil)
So(text, ShouldEqual, "Hello")
})
Convey("POST", func() {
res, err := http.Post(serverURL+"/", "appliaction/json", strings.NewReader("{}"))
defer func() { _ = res.Body.Close() }()
So(err, ShouldBeNil)
So(res.StatusCode, ShouldEqual, http.StatusOK)
text, err := readBody(res)
So(err, ShouldBeNil)
So(text, ShouldEqual, `{"value":"Hello"}`)
})
})
testing
t.Run("api test ===> e2e test", func(t *testing.T) {
t.Run("GET", func(t *testing.T) {
e := httpexpect.New(t, serverURL)
e.GET("/").
Expect().
Status(http.StatusOK).
Text().Equal("Hello")
})
t.Run("POST", func(t *testing.T) {
e := httpexpect.New(t, serverURL+"/")
e.POST("/").
Expect().
Status(http.StatusOK).
JSON().
Object().ContainsKey("value").ValueEqual("value", "Hello")
})
})
(補足)CIでのReporting
Brainpad では CircleCI を利用しています。その場合にテストレポートの形式を junit 形式に変換が必要です。(ginkgo は出力が独自)
コードとコマンド
ginkgo はコードに手を入れないといけないため、癖がある。
func TestFoo(t *testing.T) {
RegisterFailHandler(Fail)
junitReporter := reporters.NewJUnitReporter("junit.xml")
RunSpecsWithDefaultAndCustomReporters(t, "Foo Suite", []Reporter{junitReporter})
}
convey, testing は go-junit-report を利用すればよく、コードに変更は不要
go test -v 2>&1 | go-junit-report > report.xml
(3) AcceptanceTest(end-to-end Test)
golang の testing framework でどうであるかの検証は、以下の点から不毛だと思われるため評価しない。
(※ 適切なテスト設計と自動化の配備について考える事の方が重要なためtesting frameworkを評価の重要度低めと判断)
- ginkgo,gomega は agouti で利用すること出来ます。
しかしブラウザ/スマートフォンアプリの e2e として利用する場合 agouti が良いとは言いにくいと思っています。- そもそも golang でe2eしたい人はいるのだろうか?(懐疑的な気持ち)
- Fixing a Test Hourglass から、BDDやIntegration Testの自動化Testの時間 > AT,e2eTest時間 が望ましい。
まとめ
- testing でBDDまでは十分に行える
- assertion が弱いので
httpexpect
を利用するといいと思う
- assertion が弱いので
- BDDでは testing framework の利用と言うよりは、テスト設計/記述者の力が大事
- 言語は問わずに、やった方が良い
- AcceptanceTest,e2eTest,ManualTest の時間は短いことは大正義
Run Result