GOのテストフレームワークtestifyの使い方

便利なassertion

EqualValues

  • 中身の値が同じことを検証できる
  • named typeでそのままで比較ができずに、キャストが必要なときに便利
  • 否定版のNotEqualValuesもある
type Name string

// Equalを使うとキャストが必要
assert.Equal(t, "john", string(Name("john")))

// キャスト不要
assert.EqualValues(t, "john", Name("john"))

構造化されたテスト

*testing.T.Run

  • テストをネストさせて構造化したいときは*testing.T.Runメソッドが使える
  • testifyというかgoのtestのデフォルト機能
func TestApiRes(t *testing.T) {
  t.Run("return ok status", func(t *testing.T) {
    ... 
  })

  t.Run("return correct res", func(t *testing.T) {
    t.Run("some expectations", func(t *testing.T) {
      ...
    }
  })
}

channelが絡んだテスト

channelに値が投げ込まれたことをテストする

  • testifyはLenというアサーションメソッドが用意されており、これはchannelにも使用できる
  • ただしchannelに何かが送信された場合はそのチャンネルのキャパシティを明示的に1以上に指定しなければならない
  • channelはデフォルトでキャパシティは0
  • 0キャパのchannelのlenは常に0になるらしい
  • この問題はLen以外のアサーションを用いても起こる
zeroChan := make(chan interface{})
oneChan := make(chan interface{}, 1)

zeroChan <- true
oneChan <- true

assert.Len(1, zeroChan) // fail
assert.Len(1, oneChan) // success

Mockの使い方

基本的には公式のとおりにやればいい。

OnとReturnでのMockの挙動設定

  • 以下のようにmockのメソッドを定義しておく
  • Calledというメソッドがmockにその引数で呼ばれたことを記憶させる
  • args.int(0)みたいなのが何してるかは後で説明する
func (o *MyTestObject) SavePersonDetails(firstname, lastname string, age int) (int, error) {
  args := o.Called(firstname, lastname, age)
  return args.Int(0), args.Error(1)
}
  • 実際にmockの挙動を設定する
mo := new(MyTestObject)
mo.On("SavePersonDetails", "fstNm", "lstNm", 20).Return(30, nil)
  • OnSavePersonDetailsというメソッドをfstNm, lstNm, 20を引数として呼んだときに、Return30, nilという値を返すという設定ができている
  • 先のmockのメソッド定義で出てきたargs.Int(0)というものがReturnで渡された値を使ってメソッドの戻り値を設定している
  • argsというのがReturnに渡された引数、つまりこの場合は30, nil
  • args.Int(0)30, nilの0番目、つまり30をIntにキャストして戻り値とするという意味
    • goは型が弱いので用意されているヘルパー関数を使って明示的にキャストしてやる必要がある
    • それがキャストできないような場合、つまりReturn("a")のときにargs.Int(0)とかやるとpanic

設定したmockの挙動が想定どおりに呼ばれたかをテストする

  • 例えば以下のようにmockを設定する
mo := new(MyTestObject)
mo.On("SavePersonDetails", "fstNm", "lstNm", 20).Return(30, nil)
  • これがこの通り呼ばれたことをテストしたい
  • つまりmoSavePersonDetailsメソッドがfstNmlstNmを引数として呼ばれたことをテストしたい
  • mockのAssertExpectationsメソッドを使用する
mo.AssertExpectations(t) // tは*testing.T
  • AssertExpectationsはただ呼ばれたことを検証するメソッドだが、他にも回数を指定して検証を行えるAssertNumberOfCallsなどがあるのでそれはユースケースに応じて

可変長引数を取るメソッドをモックする

  • 例えば以下のようなinterfaceがあったとする
type Obj interface {
    Do(ags ...string)
}
  • このinterfaceをMockするときに注意する必要がある
  • 具体的にはモックメソッド内では可変長引数は配列として扱われるのでそれを展開してCalledを渡す必要がある
type MockObj struct {
    mock.Mock
}

func (m *MockObj) Do(ags ...string) {
  args := m.Called(ags...) // ここで展開
}
// Do("a", "b")が呼ばれるものとしてその挙動をモックしたいとき

m := new(MockObj)
m.On("Do", "a", "b") 

条件に合致したオブジェクトが引数になって呼ばれたかをチェックする

  • 以下の奴らがいたとする
type Person struct {
    age int
}

type Repo interface {
    Save(p Person) 
}

// ...その他mockの設定
  • このRepoをモックしたい
  • モックの設定としてはPerson.age >= 20なpが引数として呼ばれることを設定したい
  • ただし実際のageはテストするまで確定しない
  • このときにmockRepo.On("Save", Person{age: 30})みたいなことはできない
  • なぜなら実際のageは30ではない場合があるからこの設定でテストすると設定されてない引数で呼ばれたよエラーが出る
  • そのときにMatchedByというやつを使うと行ける
  • モックに渡す引数の型 => boolな関数をこいつに渡すとその関数がtrueを吐くときにモックの設定が発火する
mockRepo := new(MockRepo)
mockRepo.On("Save", MatchedBy(func(p Person) bool { return p.age >= 20 }))
  • これでage >= 20Personが呼ばれることがmockできた

mockの自動生成

  • testifyで使用するためのmockは毎回ある程度決まりきったことを自分で記述する必要がありめんどくさい
  • mockeryというツールを使えば自動で生成できる
  • プロジェクト直下でmockery --allとコマンドを打つと./mocks以下に各インターフェースを実装したモックが生成される

mockeryのオプション

  • --all: 実行したディレクトリ以下のすべてのインターフェース用のモックを生成する
  • --inpackage: モックを各インターフェースが定義されているディレクトリと同じ場所に生成する

ginと併用したテスト

*gin.Contextを引数に取るハンドラ関数のテスト

  • 以下のようなハンドラ関数があるとする
hello := func(c *gin.Context) {
      c.JSON(http.StatusOK, gin.H{
          "hello": "world",
      })
   }
  • この関数に関して以下を検証するテストを書く
    1. レスポンスステータス
    2. レスポンスボディ
func TestGetRecentlyReadTag(t *testing.T) {
    assert := assert2.New(t)
    req, _ := http.NewRequest("GET", "/hello", nil)
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
    c.Request = req

    hello(c)

    var res map[string]string
    _ = json.Unmarshal(w.Body.Bytes(), &res)

    assert.Equal(w.Code, http.StatusOK)
    assert.Equal("world", res["hello"])
}
ラズパイ向けにCGO_ENABLEDしながらGoをクロスコンパイルするDockerfile例 Go tips Goにproperty based testingを布教したい
View Comments
There are currently no comments.