カテゴリー: BackEnd

Go言語でテスト作成 testifyの基本的な使い方

はじめに

こんにちは、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コマンドで導入可能です。

$ 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を作成します。

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文を書きます。

package stringutil

import (
  "testing"
  "github.com/stretchr/testify/assert"
)

func TestReverse(t *testing.T) {
  assert.Equal(t, Reverse("abc"), "cba")
}

上記のように記述し、下記のコマンドでテストが実行されます。

$ go test
PASS
ok   github.com/user/stringutil 0.022s

assertion紹介

testifyでは上記の他にもたくさんのassartionがあります。
使用頻度の高いAssertを下記に記述します。

ElementsMatch

要素が同様かの確認(順番は無関係)

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

第二引数に第三引数の要素が含まれているかの確認を行います。

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はその逆です。

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…厳密な比較、型の比較を行う

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…エラーの文字列が望まれる文字列か

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を作成しレスポンスの変更をしております。

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がスイートの各テストの前に実行されます。

type SetupTestSuite interface {
    SetupTest()
}

スイートの各テストの後に実行するインターフェース

下記のTearDownTestSuiteがスイートの各テストの前に実行されます。

type TearDownTestSuite interface {
    TearDownTest()
}

Suiteを使ってみる

Suiteのテストも go test で実行できます。
Suiteはストアした情報を一度クリアする、特定のユーザーの初期値をストアする、等で利用できます。
今回はシンプルに動作の確認を取れるサンプルを作成しました。以下のサンプルコードをご確認ください。

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))
}

実行すると下記のように結果が表示されるかと思います。

&go test
-5
-5
10
5
PASS
ok

・setupの箇所がテストごとに呼ばれ、初期値が設定されていることの確認
・teardownのログ出力で、suite.takeOverCountがそのまま引き継がれている事が確認
上記が確認取れました。

最後に

今回はtestifyの使用例を交えて説明させていただきました。
単体テストでカバレッジレポートの出力もGoでは簡単に作成可能のようです。
実装に比べて手を抜きがちになってしまいますが、testifyで少しでも運用を楽にできればと思います。

私の次回の記事では
・BDDについて
・gingko gomega
について記事を作成させていただこうと思っております。またよろしくお願い致します。

Go記事の連載などは、こちらをご覧ください。

おすすめ書籍

     

suzuki

シェア
執筆者:
suzuki
タグ: golangGo言語

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前