はじめに
こんにちは、suzukiです。
先週から引き続きgo強化週間です。新しい言語の習得に四苦八苦しております。
第5回目の発表内容は、testifyを利用してテストの作成について触れさせていただきます。
Goテストフレームワークのスター数
Goのテストフレームワークについて、参考リンクを色々探していたのですが、まだまだ群雄割拠の時代です。
Goではtestingパッケージを標準で提供していますが、やや使い方に難があります。そこで、行いたいテストによって他のフレームワークを導入されているようです。
また参考リンク内でよく「デファクトスタンダートがない」等の記述もあったため、スター数が多めのリポジトリ検索をしました。
リポジトリ | star | contributor | 説明 |
stretchr / testify | 5874 | 121 | 標準的なライブラリでうまく動作する共通のアサーションとモックを持つツールキット |
onsi/ginkgo | 2357 | 77 | BDD Testing Framework for Go http://onsi.github.io/ginkgo/ |
360EntSecGroup-Skylar / goreporter | 2190 | 11 | A Golang tool that does static analysis, unit testing, code review and generate code quality report. |
検索結果の中で私が普段Swiftの開発で使っているXCTestと近しいと思われるassartionを使えるリポジトリとしてtestifyをメインに進めて行こうと思います。
testifyについて
それではtestifyについて説明させていただきます。まずはtestifyで出来ることですが、
・Easy assertions
・Mocking
・Testing suite interfaces and functions
上記の三つの機能があります。
また、GitHubを確認いただくとわかるのですが、使用例を含めてReadMeが書いてあるため、導入と確認が簡単です!
testifyを導入する方法
実際に導入をして行きましょう。go getコマンドで導入可能です。
1 | $ go get github.com/stretchr/testify |
testifyの下記パッケージで取得できます。
・github.com/stretchr/testify/assert
・github.com/stretchr/testify/mock
・github.com/stretchr/testify/http
assartionについて
assertionの基本的な使い方は、プログラムの任意の場所に「その場所で成立していることが期待される条件式」を記述するというものです。
in-outのある関数が想定通りに動いているかの確認等を行います。
今回はHow to Write Go Codeに従いReverse関数を作成して見ましょう。
inで受け取った文字列を順番を逆順にして文字列を返却する関数Reverseを作成します。
1 2 3 4 5 6 7 8 9 10 | package stringutil // Reverse returns its argument string reversed rune-wise left to right. func Reverse(s string) string { r := []rune(s) for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } |
この関数の機能が問題なく動作していることの確認としては、 abc を入力したら cba が返却されるということを期待してassert文を書きます。
1 2 3 4 5 6 7 8 9 10 | package stringutil import ( "testing" "github.com/stretchr/testify/assert" ) func TestReverse(t *testing.T) { assert.Equal(t, Reverse("abc"), "cba") } |
上記のように記述し、下記のコマンドでテストが実行されます。
1 2 3 | $ go test PASS ok github.com/user/stringutil 0.022s |
assertion紹介
testifyでは上記の他にもたくさんのassartionがあります。
使用頻度の高いAssertを下記に記述します。
ElementsMatch
要素が同様かの確認(順番は無関係)
1 2 3 4 5 6 7 8 | func TestElementsMatch(t *testing.T){ assert.ElementsMatch(t, [...]int{1,1,2,5,3,4},[...]int{2,1,1,4,3,5}) assert.ElementsMatch(t, [...]string{"Hello", "World"},[...]string{"World", "Hello"}) //失敗要素数が異なる assert.ElementsMatch(t, [...]int{1,1,2,5,3,4},[...]int{2,1,4,3,5} ) //失敗要素の内容が異なる assert.ElementsMatch(t, [...]int{1,1,2,5,3,4},[...]int{2,2,1,4,3,5} ) } |
Contains
第二引数に第三引数の要素が含まれているかの確認を行います。
1 2 3 4 5 | func TestContains(t *testing.T){ assert.Contains(t, [...]int{1,1,2,5,3,4},5) assert.Contains(t, [...]string{"Hello", "World"}, "Hello") assert.Contains(t, "こんにちはsuzukiです。ただいまGo勉強中", "Go") } |
Empty&NotEmpty
nil, “”, false, 0 要素が存在しないか などを確認 Notはその逆です。
1 2 3 4 5 6 | func TestEmpty(t *testing.T){ assert.Empty(t,0) assert.Empty(t,[...]int{}) assert.NotEmpty(t,1) assert.NotEmpty(t,[...]int{1}) } |
Equal&NotEqual (EqualValues Exactly)
Equal…右辺と左辺の値が等しいか確認(使用頻度はかなり高く、いろんなassartionの代用が可能)
NotEqual…右辺と左辺の値が等しくない事を確認
EqualValues…内容の比較(型の比較は行わない。ObjectsAreEqualValuesで比較を行う。nilの場合の扱いがEqualと少し異なる)
Exactly…厳密な比較、型の比較を行う
1 2 3 4 5 6 7 8 9 10 11 | func TestEqual(t *testing.T){ //失敗 assert.Equal(t,int32(10203),int64(10203)) //成功 assert.EqualValues(t,int32(10203),int64(10203)) //失敗 assert.Exactly(t,int32(10203),int64(10203)) assert.Equal(t,"テスト","テスト") assert.NotEqual(t,3.14,3.1415) assert.NotEqual(t,"テスト","test") } |
Error&NoError&EqualError
・Error…エラーが存在しているか
・NoError…エラーが存在していないか
・EqualError…エラーの文字列が望まれる文字列か
1 2 3 4 5 6 7 8 9 10 | func TestError(t *testing.T){ actualObj, err := SomeFunction() if assert.Error(t, err) { assert.Equal(t, expectedError, err) } if assert.NoError(t, err) { assert.Equal(t, expectedObj, actualObj) } assert.EqualError(t, err, expectedErrorString) } |
Mockについて
assertionでは、値が望まれる形であるかを確認する方法をまとめました。
しかしながら、実際にプログラムを書くとin-outでinの要素を外部から取得するため、不確定な場合がよくあります。
テストの内容を不確定な場合に合わせて変更するのは、テストの運用上間違っています。
Mockを使い不確定な環境をコードから作成し、機能の確認を行うことができます。
Mockを使ってみる
三角形の構造体Triangleに
・CalcArea関数 プロパティの底辺X高さ/2を返却する関数
・RandomArea関数 ランダムな値から面積を返却する関数
以下のコードは実際にMockを作成しレスポンスの変更をしております。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | import ( "fmt" "testing" "math/rand" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) //トライアングルの構造体の作成 type Triangle struct { bottom int height int } //トライアングルに関数を作成 func (triangle *Triangle) CalcArea() int { return triangle.bottom * triangle.height / 2 } func (triangle *Triangle) RandomArea() int { return rand.Int() * rand.Int() / 2 } //インターフェース CalcArea RandomArea type TriangleInterface interface { CalcArea() int RandomArea() int } func NewTriangle(height , bottom int ) TriangleInterface { t := Triangle { height : height ,bottom : bottom} var triangle TriangleInterface = &t return triangle } //Mockの使用準備 ストラクトでmock.Mockを記述 type TriangleMock struct{ mock.Mock } //mockからCalcAreaを呼び出し func (mock *TriangleMock) CalcArea() int{ args := mock.Called() return args.Int(0) } //mockからRandomAreaを呼び出し func (mock *TriangleMock) RandomArea() int{ args := mock.Called() return args.Int(0) } type GetTriangleService struct { triangleInterface TriangleInterface } 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 } //triangleInterfaceのRandomAreaを返却 func (service *GetTriangleService) RandomArea() int{ area := service.triangleInterface.RandomArea() return area } func TestCalculation(t *testing.T) { assert := assert.New(t) //triangleを作成 triangle := NewGetTriangleService(50,50) assert.Equal(triangle.CalcArea(),1250) //Mockを定義 triangleMock := new(TriangleMock) service := GetTriangleService{ triangleInterface : triangleMock , } //CalcAreaのモック triangleMock.On("CalcArea").Return(105) calcArea := service.CalcArea() assert.Equal(calcArea,105) fmt.Println(calcArea) //RandomAreaのモック triangleMock.On("RandomArea").Return(100) randomArea := service.RandomArea() assert.Equal(randomArea,100) fmt.Println(randomArea) } |
suiteについて
testifyではsuitePackageを利用することでtest suiteを作成することが可能です。
Xcodeでもデフォルトで用意されている下記の2点について簡単に紹介します。
・スイートの各テストの前に実行
・スイートの各テストの後に実行
スイートの各テストの前に実行するインターフェース
下記のSetupTestSuiteがスイートの各テストの前に実行されます。
1 2 3 | type SetupTestSuite interface { SetupTest() } |
スイートの各テストの後に実行するインターフェース
下記のTearDownTestSuiteがスイートの各テストの前に実行されます。
1 2 3 | type TearDownTestSuite interface { TearDownTest() } |
Suiteを使ってみる
Suiteのテストも go test で実行できます。
Suiteはストアした情報を一度クリアする、特定のユーザーの初期値をストアする、等で利用できます。
今回はシンプルに動作の確認を取れるサンプルを作成しました。以下のサンプルコードをご確認ください。
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 | package stringutil import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) //Suiteの構造体の作成 type CountTestSuite struct { suite.Suite //testCountはSetupTest()で0で初期化される想定 testCount int takeOverCount int } // スイートの各テストの前に実行されます。初期値の設定などをこちらで設定します。 func (suite *CountTestSuite) SetupTest() { suite.testCount = 0 } // スイートの各テストの後に実行されます。今回はログ出しだけして見ました。 func (suite *CountTestSuite) TearDownTest() { fmt.Println(suite.testCount) fmt.Println(suite.takeOverCount) } func (suite *CountTestSuite) TestCountUp() { assert.Equal(suite.T(), 0, suite.testCount) suite.testCount = suite.testCount + 10 suite.takeOverCount = suite.takeOverCount + 10 assert.Equal(suite.T(), 10, suite.testCount) } func (suite *CountTestSuite) TestCountDown() { suite.testCount = suite.testCount - 5 assert.Equal(suite.T(), -5, suite.testCount) suite.takeOverCount = suite.takeOverCount - 5 } //Suiteテストの実行 func TestExampleTestSuite(t *testing.T) { suite.Run(t, new(CountTestSuite)) } |
実行すると下記のように結果が表示されるかと思います。
1 2 3 4 5 6 7 | &go test -5 -5 10 5 PASS ok |
・setupの箇所がテストごとに呼ばれ、初期値が設定されていることの確認
・teardownのログ出力で、suite.takeOverCountがそのまま引き継がれている事が確認
上記が確認取れました。
最後に
今回はtestifyの使用例を交えて説明させていただきました。
単体テストでカバレッジレポートの出力もGoでは簡単に作成可能のようです。
実装に比べて手を抜きがちになってしまいますが、testifyで少しでも運用を楽にできればと思います。
私の次回の記事では
・BDDについて
・gingko gomega
について記事を作成させていただこうと思っております。またよろしくお願い致します。
Go記事の連載などは、こちらをご覧ください。