はじめに
こんにちはsuzukiです。Go言語の連載9回目です。
今回はライブラリのGinkgoを利用してBDDについて説明いたします。
BDDとは
BDD(BehaviorDrivenDevelopment)とはビヘイビア駆動開発と呼ばれる開発方法です。
- 実現したい機能の振る舞い(要求仕様)を決める
- 振る舞い(要求仕様)を元にテストコードを作成
- テストコードを満たすための機能を開発
- リファクタリングを行う
上記のように開発していきます。
振る舞いの意味については色々な意見があるかと思いますが、この記事では要求仕様として進めさせていただきます。
Ginkgoについて
今回使用するGinkgoについて主要な関数についてまとめさせていただきます。
Ginkgoの概要
BDDを進める上でテストコードの作成を行いやすくするためのライブラリです。
GomegaというMatcherのライブラリと併用して使うことでより自然言語でテストが書きやすくなります。GinkgoにはGomegaと依存関係はないですが、本家のサイトでは使われているので今回こちらも使用していきます。
Ginkgoは簡単にテストスイートの作成とテストスペックのテンプレートの作成が行えます。
Describe
テストスペックを作成すると元から入っている関数です。こちらのテキスト部分でテストの対象が何かを記述します。
こちらの中に要求仕様をまとめて記述を行ったりBeforeEachなどの関数をネストできます。
Context
こちらも機能的にはDescribeと同様ですが、特定の条件が何かを記述します。例えばユーザーの状態等をここで定義します。
It
こちらのItの中でテスト対象のアウトプットが何かを記述します。テストの対象が実際にどのような動きをしてほしいかなど。
同期で実行されるため、非同期の場合のテストを行う場合はDoneを利用します。
JustBeforeEach
BeforeEachブロックがすべて実行され、Itブロックが実行される直前に実行されます。 この事実を利用してBookの仕様を整理することができます。
BeforeEach
BeforeEachブロックはItブロックの前に実行されます。
ネストされたDescribeブロックとContextブロックで複数のBeforeEachブロックが定義されている場合、最も外側のBeforeEachブロックが最初に実行されます。
AfterEach
AfterEachブロックはItブロックの後に実行されます。
ネストされたDescribeブロックとContextブロックで複数のBeforeEachブロックが定義されている場合、最も外側のBeforeEachブロックが最初に実行されます。
BeforeSuite
BeforeSuiteブロックは、テストスペックが実行される前に一度だけ実行されます。
並列で複数のテストスペック実行する場合、各並列ノードプロセスはBeforeSuiteを呼び出します。
AfterSuite
AfterSuiteブロックは、テストスペックが成功したか失敗したかにかかわらず、すべての実行後に実行されます。 さらに、Ginkgoが割り込み信号(^ C)を受信すると、終了する前にAfterSuiteを実行しようとします。
Gomegaについて
GomegaライブラリはBDD向けのアサーションを提供してくれます。簡単にですが説明させていただきます。
Gomegaの概要
BDDを進める上でIt内のアサーションをわかりやすく作成することができます。
Testifyのアサーションに比べ、BDDを行いやすくするように直感的にわかりやすいアサーションが多くあります。
基本的に (ACTUAL).Should(アサーション(EXPECTED))という形で使われます。
Equal(EXPECTED)
ACTUALとEXPECTEDが等しいか比較を行います。
厳密な比較を行い、対象の型が同じかも比較されます。
BeEquivalentTo
Equal同様にACTUALとEXPECTEDが等しいか比較を行います。
違いとしては比較を行う前に、ACTUALの型をEXPECTEDの型に変換します。
したがって型自体の違いはあまり意識しません。
BeNil()
ACTUALがnilであるかを比較します。
Equalでnilを比較するとエラーになるためnilを比較する場合にはこちらを利用します。
BeZero()
ACTUALが0であるかを比較します。nilの場合も0として扱われ成功します。
ContainSubstring(STRING, ARGS…)
ACTUALに生成された部分文字列が含まれている場合は成功します。
ContainElement(ELEMENT)
ACTUALにELEMENTと等しい要素が含まれていると成功します。 ACTUALは配列、スライス、またはマップでなければなりません。それ以外はエラーです。 マップの場合、ContainElementはマップの値を検索します(キーではありません)。
BDDを試してみる
それでは実際にテストを作成していきます。
GinkGo導入方法
go get コマンドでライブラリの導入可能です。
Gomegaも合わせて取得しましょう。
1 2 | $ go get github.com/onsi/ginkgo/ginkgo $ go get github.com/onsi/gomega/... |
テストスイート作成
Ginkgoのテストスイート作成機能を利用してテストスイートを作成します。
1 2 | $ cd 機能を開発するディレクトリ $ ginkgo bootstrap |
こちらのコマンドでディレクトリ名_suite_test.goというテストスイートが作成されます。
今回は/Triangleというディレクトリに下記のファイルが作成されました。
1 2 3 4 5 6 7 8 9 10 11 | import ( "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) func TestTriangle(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Triangle Suite") } |
こちらでテストスイートの作成ができました。
スペックテンプレート作成
Ginkgoのテストスペック作成機能を利用してテストスペックのテンプレートを作成します。
1 | $ ginkgo generate triangle |
triangleは開発を行う機能名に置き換えてください。
こちらのコマンドで下記のファイルが作成されました。
1 2 3 4 5 6 7 8 9 10 11 12 | 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が面積の計算を行った時に
- 底辺と高さが正の整数であるときに、面積を計算すると、底辺*高さ/2になる
- 底辺か高さが負の整数であるときに、面積を計算すると、0になる
- 底辺か高さが0であるときに、面積を計算すると、0になる
という要求仕様があるとした時に下記のように記述できます。
今回はアサーションを複数書いていますが、意味は一緒です。
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 | 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()) }) }) }) }) |
Triangleの実装
今回の記事では詳細に触れる必要はあまりないと思っていますが、
前回の記事で作成を行ったトライアングルを元に下記のように実装しました。
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 | //トライアングルの構造体の作成 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 } |
Testの実行
Ginkgoのテストコマンドはginkgoです。こちらでテストの実行を行うことができます。
テスト失敗
個人的にBDDのメリットはテストの失敗した時の内容がとても便利だと思っております。
今回はわざとテストを失敗させました。
要求仕様のうちどれだけの機能が実装できていないかが簡単にわかります。
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 | $ 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 |
テスト成功
こちらは成功したパターンです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ 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では進めたら納得しやすい値を簡単に出せそうだなと思いました。