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