こんにちはsuzukiです。Go言語の連載9回目です。
今回はライブラリのGinkgoを利用してBDDについて説明いたします。
BDD(BehaviorDrivenDevelopment)とはビヘイビア駆動開発と呼ばれる開発方法です。
上記のように開発していきます。
振る舞いの意味については色々な意見があるかと思いますが、この記事では要求仕様として進めさせていただきます。
今回使用するGinkgoについて主要な関数についてまとめさせていただきます。
BDDを進める上でテストコードの作成を行いやすくするためのライブラリです。
GomegaというMatcherのライブラリと併用して使うことでより自然言語でテストが書きやすくなります。GinkgoにはGomegaと依存関係はないですが、本家のサイトでは使われているので今回こちらも使用していきます。
Ginkgoは簡単にテストスイートの作成とテストスペックのテンプレートの作成が行えます。
テストスペックを作成すると元から入っている関数です。こちらのテキスト部分でテストの対象が何かを記述します。
こちらの中に要求仕様をまとめて記述を行ったりBeforeEachなどの関数をネストできます。
こちらも機能的にはDescribeと同様ですが、特定の条件が何かを記述します。例えばユーザーの状態等をここで定義します。
こちらのItの中でテスト対象のアウトプットが何かを記述します。テストの対象が実際にどのような動きをしてほしいかなど。
同期で実行されるため、非同期の場合のテストを行う場合はDoneを利用します。
BeforeEachブロックがすべて実行され、Itブロックが実行される直前に実行されます。 この事実を利用してBookの仕様を整理することができます。
BeforeEachブロックはItブロックの前に実行されます。
ネストされたDescribeブロックとContextブロックで複数のBeforeEachブロックが定義されている場合、最も外側のBeforeEachブロックが最初に実行されます。
AfterEachブロックはItブロックの後に実行されます。
ネストされたDescribeブロックとContextブロックで複数のBeforeEachブロックが定義されている場合、最も外側のBeforeEachブロックが最初に実行されます。
BeforeSuiteブロックは、テストスペックが実行される前に一度だけ実行されます。
並列で複数のテストスペック実行する場合、各並列ノードプロセスはBeforeSuiteを呼び出します。
AfterSuiteブロックは、テストスペックが成功したか失敗したかにかかわらず、すべての実行後に実行されます。 さらに、Ginkgoが割り込み信号(^ C)を受信すると、終了する前にAfterSuiteを実行しようとします。
GomegaライブラリはBDD向けのアサーションを提供してくれます。簡単にですが説明させていただきます。
BDDを進める上でIt内のアサーションをわかりやすく作成することができます。
Testifyのアサーションに比べ、BDDを行いやすくするように直感的にわかりやすいアサーションが多くあります。
基本的に (ACTUAL).Should(アサーション(EXPECTED))という形で使われます。
ACTUALとEXPECTEDが等しいか比較を行います。
厳密な比較を行い、対象の型が同じかも比較されます。
Equal同様にACTUALとEXPECTEDが等しいか比較を行います。
違いとしては比較を行う前に、ACTUALの型をEXPECTEDの型に変換します。
したがって型自体の違いはあまり意識しません。
ACTUALがnilであるかを比較します。
Equalでnilを比較するとエラーになるためnilを比較する場合にはこちらを利用します。
ACTUALが0であるかを比較します。nilの場合も0として扱われ成功します。
ACTUALに生成された部分文字列が含まれている場合は成功します。
ACTUALにELEMENTと等しい要素が含まれていると成功します。 ACTUALは配列、スライス、またはマップでなければなりません。それ以外はエラーです。 マップの場合、ContainElementはマップの値を検索します(キーではありません)。
それでは実際にテストを作成していきます。
go get コマンドでライブラリの導入可能です。
Gomegaも合わせて取得しましょう。
$ go get github.com/onsi/ginkgo/ginkgo $ go get github.com/onsi/gomega/...
Ginkgoのテストスイート作成機能を利用してテストスイートを作成します。
$ cd 機能を開発するディレクトリ $ ginkgo bootstrap
こちらのコマンドでディレクトリ名_suite_test.goというテストスイートが作成されます。
今回は/Triangleというディレクトリに下記のファイルが作成されました。
import ( "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) func TestTriangle(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Triangle Suite") }
こちらでテストスイートの作成ができました。
Ginkgoのテストスペック作成機能を利用してテストスペックのテンプレートを作成します。
$ ginkgo generate triangle
triangleは開発を行う機能名に置き換えてください。
こちらのコマンドで下記のファイルが作成されました。
package stringutil_test import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/user/stringutil" ) var _ = Describe("Triangle", func() { })
こちらのvar _ = Describe("Triangle", func() { }
の中にスペックの内容を記述します。
今回の仕様がtriangleが面積の計算を行った時に
という要求仕様があるとした時に下記のように記述できます。
今回はアサーションを複数書いていますが、意味は一緒です。
var _ = Describe("Triangle", func() { triangle := NewGetTriangleService(50,50,) minusTriangle := NewGetTriangleService(50,-1,) zeroTriangle := NewGetTriangleService(50,0,) Describe("面積を求める関数", func() { Context("底辺と高さが正の整数", func() { It("底辺*高さ/2が返却される", func() { Expect(triangle.CalcArea()).To(Equal(1250)) Ω(triangle.CalcArea()).Should(Equal(1250)) }) }) Context("底辺か高さが0であるときに", func() { It("0が返却される", func() { Expect(minusTriangle.CalcArea()).To(BeZero()) Ω(minusTriangle.CalcArea()).Should(BeZero()) }) }) Context("底辺か高さが負の整数", func() { It("0が返却される", func() { Expect(zeroTriangle.CalcArea()).To(BeZero()) Ω(zeroTriangle.CalcArea()).Should(BeZero()) }) }) }) })
今回の記事では詳細に触れる必要はあまりないと思っていますが、
前回の記事で作成を行ったトライアングルを元に下記のように実装しました。
//トライアングルの構造体の作成 type Triangle struct { bottom int height int } type TriangleInterface interface { CalcArea() int } type GetTriangleService struct { triangleInterface TriangleInterface } //テストで記述している機能 func (triangle *Triangle) CalcArea() int { //負の値だったら0を返却 if triangle.bottom < 0 || triangle.height < 0 { return 0 }else if triangle.bottom == 0 || triangle.height == 0{ return 0 } return triangle.bottom * triangle.height / 2 } func NewGetTriangleService(height,bottom int) GetTriangleService { return GetTriangleService { triangleInterface : NewTriangle(height,bottom,), } } //triangleInterfaceのCalcAreaを返却 func (service *GetTriangleService) CalcArea() int{ area := service.triangleInterface.CalcArea() return area } func NewTriangle(height , bottom int ) TriangleInterface { t := Triangle { height : height ,bottom : bottom} var triangle TriangleInterface = &t return triangle }
Ginkgoのテストコマンドはginkgoです。こちらでテストの実行を行うことができます。
個人的にBDDのメリットはテストの失敗した時の内容がとても便利だと思っております。
今回はわざとテストを失敗させました。
要求仕様のうちどれだけの機能が実装できていないかが簡単にわかります。
$ ginkgo Running Suite: Triangle Suite ============================= Random Seed: 1540731683 Will run 3 of 3 specs • ------------------------------ • Failure [0.000 seconds] Triangle /Users/macbook007/go/src/github.com/user/triangle/triangle_test.go:10 面積を求める関数 /Users/macbook007/go/src/github.com/user/triangle/triangle_test.go:15 底辺か高さが0であるときに /Users/macbook007/go/src/github.com/user/triangle/triangle_test.go:22 0が返却される [It] /Users/macbook007/go/src/github.com/user/triangle/triangle_test.go:23 Expected : 25 to be zero-valued /Users/macbook007/go/src/github.com/user/triangle/triangle_test.go:25 ------------------------------ • Summarizing 1 Failure: [Fail] Triangle 面積を求める関数 底辺か高さが0であるときに [It] 0が返却される /Users/macbook007/go/src/github.com/user/triangle/triangle_test.go:25 Ran 3 of 3 Specs in 0.001 seconds FAIL! -- 2 Passed | 1 Failed | 0 Pending | 0 Skipped --- FAIL: TestTriangle (0.00s) FAIL Ginkgo ran 1 suite in 1.193858538s Test Suite Failed
こちらは成功したパターンです。
$ ginkgo Running Suite: Triangle Suite ============================= Random Seed: 1540731716 Will run 3 of 3 specs ••• Ran 3 of 3 Specs in 0.000 seconds SUCCESS! -- 3 Passed | 0 Failed | 0 Pending | 0 Skipped PASS Ginkgo ran 1 suite in 1.170715354s Test Suite Passed
今回こちらでテストが通過しました。
この後リファクタリングを行い開発が完了します。
今回のテストでは小規模な開発のため、テストを一回行っただけです。
大規模な開発であれば、テストを行うことで進捗率の確認であったり、既存の機能にデグレが発生していないかなどもこまめに確認が取れます。
いかがでしたしょうか、私はプログラミングの進捗を報告することがよくあります。
まとめるために時間がかかったり、正確に伝わらなかったりするとストレスを感じるのですが、BDDでは進めたら納得しやすい値を簡単に出せそうだなと思いました。