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

Kazuki Moriyama (森山 和樹)
便利な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)
- Onで- SavePersonDetailsというメソッドを- fstNm, lstNm, 20を引数として呼んだときに、- Returnで- 30, 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)
- これがこの通り呼ばれたことをテストしたい
- つまりmoのSavePersonDetailsメソッドがfstNmとlstNmを引数として呼ばれたことをテストしたい
- 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)
}
- このRepoをモックしたい
- モックの設定としてはPerson.age >= 20なpが引数として呼ばれることを設定したい
- ただし実際のageはテストするまで確定しない
- このときにMatchedByというやつを使うと行ける
- モックに渡す引数の型 => boolな関数をこいつに渡すとその関数がtrueを吐くときにモックの設定が発火する
mockRepo := new(MockRepo)
mockRepo.On("Save", MatchedBy(func(p Person) bool { return p.age >= 20 }))
- これでage >= 20のPersonが呼ばれることが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",
      })
   }
- この関数に関して以下を検証するテストを書く
- レスポンスステータス
- レスポンスボディ
 
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"])
}












