Goテストモジュール Testifyをつかってみた

2021.09.17

「汝はテストをかかなければならぬ」

ということで、Goでシンプルにテストをかけるモジュールを探していました。

割といろんなプロジェクトでtestifyが使われていたので試してみました。

testifyは?

Goでテストが書きやすくるモジュールです。

以下のようなパッケージが含まれており、テストコードが簡潔にかけます。

  • assert package
  • require package
  • mock package
  • suite package

今回はそれぞれ試していこうと思います。

インストール

go get github.com/stretchr/testify

assert package

assertパッケージを使うとシンプルにアサーションチェックがかけます。

assertパッケージでは、テストが途中でFailしても後続の処理は続けます。

また、assertパッケージは結果を bool型で受け取ります。

以下のような関数maxに対してアサーションテストを実行してみます。

func max(a, b int) int {
 if a > b {
  return a
 }
 return b
}

テストコードのサンプル

func Test_Max_Assert1(t *testing.T) {
 assert.Equal(t, max(-1, 1), 1, "they should be equal")
 assert.NotEqual(t, max(-1, 1), -1, "they should not be equal")
}

ちなみに、失敗した場合はこんな感じで分かりやすく表示してくれます。

--- FAIL: Test_Max_Assert1 (0.00s)
    main_test.go:11:
                Error Trace:    main_test.go:11
                Error:          Not equal:
                                expected: 1
                                actual  : -1
                Test:           Test_Max_Assert1
                Messages:       they should be equal

Equal,NotEqualだけでなく便利メソッドが多く用意されています。

func Test_Max_Assert2(t *testing.T) {
 var obj []string
 assert.Nil(t, obj)
 obj = []string{"Something"}
 if assert.NotNil(t, obj) {
  assert.Equal(t, "Something", obj[0])
 }
}

require package

requireパッケージはassertパッケージと違い、結果を返さずその場でテストを終了させます。

内部的にはt.FailNow関数が呼び出されます

下記のようなテストコードの場合は、1番目のテストでFailするので、2番目のテストは実行されません。

func Test_Max_require(t *testing.T) {
 // 1
 require.Equal(t, max(-1, 1), -1, "they should be equal")
 // 2
 require.Equal(t, max(-2, 2), -2, "they should be equal")
}

mock package

mockパッケージは、実際のオブジェクトの代わりとなモックオブジェクトを簡単に書くことができます。

例えば、下記のような関数isAdultに対してテストをすることを考えます。

type user interface {
 GetAge() int
}

func isAdult(u user) bool {
 age := u.GetAge()
 if age >= 20 {
  return true
 } else {
  return false
 }
}

mockパッケージを使うと、下記のように簡単にuserモックを簡単に作成することができます。

type MockedUser struct {
 mock.Mock
}

func (m *MockedUser) GetAge() int {
 args := m.Called()
 return args.Int(0)
}

func Test_Mock(t *testing.T) {
 testObj := new(MockedUser)
 testObj.On("GetAge").Return(20)
 assert.True(t, isAdult(testObj))
 testObj.AssertExpectations(t)
}

少しややこしいですが、モックオブジェクトのGetAgeメソッドに対する戻り値は、testObj.On("GetAge").Return(20)で指定しています。

そして、args := m.Called()Calledで先ほど指定した戻り値の配列を受け取っています。

AssertExpectationsは、OnReturnで指定されたすべてのものが、実際に期待通りに呼び出されたことを保証します。

mockeryを利用すれば、モックコードを自動生成することができるそうです。

suite package

suiteパッケージでは、オブジェクト指向言語に似た形式のテストを書くことが出来ます。

例えば、テストスイートを構造体として構築し、構造体にsetup/teardown/testingなどのメソッドを作成し、go testでテストを実行することができます。

今回は、setup/teardownメソッドの実行順序を確認するために、下記のようなテストコードを書いてみました。

type MySuite struct {
 suite.Suite
 count int
}

func (suite *MySuite) SetupSuite() {
 suite.count++
 suite.T().Log(suite.count, ": SetupSuite")
}
func (suite *MySuite) TearDownSuite() {
 suite.count++
 suite.T().Log(suite.count, ": TearDownSuite")
}
func (suite *MySuite) SetupTest() {
 suite.count++
 suite.T().Log(suite.count, ": SetupTest")
}
func (suite *MySuite) TearDownTest() {
 suite.count++
 suite.T().Log(suite.count, ": TeardownTest")
}
func (suite *MySuite) BeforeTest(suiteName, testName string) {
 suite.count++
 suite.T().Log(suite.count, ": BeforeTest")
}
func (suite *MySuite) AfterTest(suiteName, testName string) {
 suite.count++
 suite.T().Log(suite.count, ": AfterTest")
}
func (suite *MySuite) TestExec1() {
 suite.count++
 suite.T().Log(suite.count, ": TestExec1")
 suite.Equal(true, true)
}
func (suite *MySuite) TestExec2() {
 suite.count++
 suite.T().Log(suite.count, ": TestExec2")
 suite.Equal(true, true)
}

// テスト実行
func TestMySuite(t *testing.T) {
 suite.Run(t, new(MySuite))
}

実行結果は下記のようになりました。

=== RUN   TestMySuite
    main_test.go:55: 1 : SetupSuite
=== RUN   TestMySuite/TestExec1
    main_test.go:63: 2 : SetupTest
    main_test.go:71: 3 : BeforeTest
    main_test.go:79: 4 : TestExec1
    main_test.go:75: 5 : AfterTest
    main_test.go:67: 6 : TeardownTest
=== RUN   TestMySuite/TestExec2
    main_test.go:63: 7 : SetupTest
    main_test.go:71: 8 : BeforeTest
    main_test.go:84: 9 : TestExec2
    main_test.go:75: 10 : AfterTest
    main_test.go:67: 11 : TeardownTest
=== CONT  TestMySuite
    main_test.go:59: 12 : TearDownSuite
--- PASS: TestMySuite (0.00s)
    --- PASS: TestMySuite/TestExec1 (0.00s)
    --- PASS: TestMySuite/TestExec2 (0.00s)

まとめ

簡単にですが、testifyの主要機能を紹介させて頂きました。

自分はプロジェクトに導入することで、テストコードの量を結構抑えられました。

どなたかの役に立てば幸いです。