CLOVER🍀

That was when it all began.

Goのテストライブラリ、testifyのassertとsuiteを試す

これは、なにをしたくて書いたもの?

Goにおける、アサーションなどの機能を提供するtestifyというライブラリがあるのを知りまして。

こちらを少し試しておきたいな、と。

testify

testifyは、以下の機能を提供するライブラリです。

GitHub - stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard library

testingパッケージを使い、go testでテストをするというのは通常のGoのテストと変わりません。

testing - The Go Programming Language

APIドキュメントはこちら。

testify - GoDoc

今回は、アサーションとテストスイート向けの機能を試していきたいと思います。

環境

今回の環境は、こちら。

$ go version
go version go1.15.6 linux/amd64

モジュールの作成。

$ go mod init testify-assert-example
go: creating new go.mod: module testify-assert-example

testifyは、1.6.1を使います。
go.mod

module testify-assert-example

go 1.15

require github.com/stretchr/testify v1.6.1

go.sum

github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

お題

アサーションはけっこうな数があるので、それをひとつひとつ試していくようなことはしません。どちらかというと、
ライブラリの使い方の雰囲気、全体感を把握したいな、と。

というわけで、テスト対象のソースコードはこれだけにします。
calc.go

package main

func Plus(a int, b int) int {
    return a + b
}

assertパッケージ

まずは、アサーションからです。testifyのassertというパッケージを使います。

assert package

assert - GoDoc

assertパッケージは、以下の機能を持ちます。

assertパッケージを使ったテストコードは、こんな感じで作成しました。
calc_test.go

package main

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

func TestPlusSimply(t *testing.T) {
    result := Plus(1, 3)
    expect := 4

    assert.Equal(t, expect, result)
}

func TestPlusAssertFailContinue(t *testing.T) {
    result1 := Plus(1, 3)
    expect1 := 4

    assert.Equal(t, expect1, result1)

    result2 := Plus(1, 3)
    badExpect2 := 5 // miss

    assert.Equal(t, badExpect2, result2)

    result3 := Plus(3, 5)
    expect3 := 8

    assert.Equal(t, expect3, result3, "not equal")

    result4 := Plus(3, 5)
    badExpect4 := 9 // miss

    assert.Equal(t, badExpect4, result4, "not equal")
}

func TestPlusFormat(t *testing.T) {
    result1 := Plus(1, 3)
    expect1 := 4

    assert.Equalf(t, expect1, result1, "not equal, collect = %d", expect1)

    result2 := Plus(1, 3)
    badExpect2 := 5 // miss

    assert.Equalf(t, badExpect2, result2, "not equal, collect = %d", 4)

    result3 := Plus(3, 5)
    expect3 := 8

    assert.Equalf(t, expect3, result3, "not equal, collect = %d", expect3)

    result4 := Plus(3, 5)
    badExpect4 := 9 // miss

    assert.Equalf(t, badExpect4, result4, "not equal, collect = %d", 8)
}

説明していきます。

最低限、testingとtestify/assertの2つが必要です。

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

使い方としては、assert.[使いたいアサーション関数]となります。

func TestPlusSimply(t *testing.T) {
    result := Plus(1, 3)
    expect := 4

    assert.Equal(t, expect, result)
}

最初の引数にtestingのT、あとは期待値、実際の値という順で書いていきます。

今回使っているのは、期待値と実際の値が等しいことを確認するEqualですね。

func Equal

ちなみに、戻り値としてはboolが返ってきます…。

4番目の引数としてカスタムメッセージを入れると、失敗時にメッセージを入れることができます。
以下では、後ろの2つにメッセージを入れました。

func TestPlusAssertFailContinue(t *testing.T) {
    result1 := Plus(1, 3)
    expect1 := 4

    assert.Equal(t, expect1, result1)

    result2 := Plus(1, 3)
    badExpect2 := 5 // miss

    assert.Equal(t, badExpect2, result2)

    result3 := Plus(3, 5)
    expect3 := 8

    assert.Equal(t, expect3, result3, "not equal")

    result4 := Plus(3, 5)
    badExpect4 := 9 // miss

    assert.Equal(t, badExpect4, result4, "not equal")
}

このテストでは、意図的に失敗するテストを書いています。テストを実行すると、こんな感じになります。

$ go test
--- FAIL: TestPlusAssertFailContinue (0.00s)
--- FAIL: TestPlusAssertFailContinue (0.00s)
    calc_test.go:24: 
            Error Trace:    calc_test.go:24
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusAssertFailContinue
    calc_test.go:34: 
            Error Trace:    calc_test.go:34
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusAssertFailContinue
            Messages:       not equal

カスタムメッセージを指定した方には、Messagesという項目が追加されます。

が、これを見たらわかるように、-vを付けなくても十分に情報が出ます。

-vを付けると、こんな感じになります。

=== RUN   TestPlusAssertFailContinue
    calc_test.go:24: 
            Error Trace:    calc_test.go:24
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusAssertFailContinue
    calc_test.go:34: 
            Error Trace:    calc_test.go:34
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusAssertFailContinue
            Messages:       not equal
--- FAIL: TestPlusAssertFailContinue (0.00s)

この時点でわかりますが、assertを使ってアサーションに失敗した場合、テストには失敗しますがテスト自体は続行します。
testingのErrorの挙動ですね。

実際、assertパッケージの各関数はErrorを使って作成されています。

https://github.com/stretchr/testify/blob/v1.6.1/assert/assertions.go#L240-L265

また、末尾がfの方の関数を使うと、カスタムメッセージのフォーマットが指定できることになっています。

func TestPlusFormat(t *testing.T) {
    result1 := Plus(1, 3)
    expect1 := 4

    assert.Equalf(t, expect1, result1, "not equal, collect = %d", expect1)

    result2 := Plus(1, 3)
    badExpect2 := 5 // miss

    assert.Equalf(t, badExpect2, result2, "not equal, collect = %d", 4)

    result3 := Plus(3, 5)
    expect3 := 8

    assert.Equalf(t, expect3, result3, "not equal, collect = %d", expect3)

    result4 := Plus(3, 5)
    badExpect4 := 9 // miss

    assert.Equalf(t, badExpect4, result4, "not equal, collect = %d", 8)
}

実行結果。

--- FAIL: TestPlusFormat (0.00s)
    calc_test.go:46: 
            Error Trace:    calc_test.go:46
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusFormat
            Messages:       not equal, collect = 4
    calc_test.go:56: 
            Error Trace:    calc_test.go:56
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusFormat
            Messages:       not equal, collect = 8

なんですが、fなしのアサーション関数でも、フォーマットは使えるっぽいですが…?

最終的には、fmt#Sprintfが使われるからですね。

https://github.com/stretchr/testify/blob/v1.6.1/assert/assertions.go#L191

testingとかと合わせるなら、フォーマットを指定する場合はf付きの方を使うのが無難でしょうか。

とまあ、雰囲気はこんな感じみたいです。その他のアサーション用の関数については、APIドキュメントを眺めましょう。

assert - GoDoc

最後に、テスト実行結果の全体を貼っておきます。

$ go test
--- FAIL: TestPlusAssertFailContinue (0.00s)
    calc_test.go:24: 
            Error Trace:    calc_test.go:24
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusAssertFailContinue
    calc_test.go:34: 
            Error Trace:    calc_test.go:34
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusAssertFailContinue
            Messages:       not equal
--- FAIL: TestPlusFormat (0.00s)
    calc_test.go:46: 
            Error Trace:    calc_test.go:46
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusFormat
            Messages:       not equal, collect = 4
    calc_test.go:56: 
            Error Trace:    calc_test.go:56
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusFormat
            Messages:       not equal, collect = 8
FAIL
exit status 1
FAIL    testify-assert-example  0.003s


## -v付き
$ go test -v
=== RUN   TestPlusSimply
--- PASS: TestPlusSimply (0.00s)
=== RUN   TestPlusAssertFailContinue
    calc_test.go:24: 
            Error Trace:    calc_test.go:24
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusAssertFailContinue
    calc_test.go:34: 
            Error Trace:    calc_test.go:34
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusAssertFailContinue
            Messages:       not equal
--- FAIL: TestPlusAssertFailContinue (0.00s)
=== RUN   TestPlusFormat
    calc_test.go:46: 
            Error Trace:    calc_test.go:46
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusFormat
            Messages:       not equal, collect = 4
    calc_test.go:56: 
            Error Trace:    calc_test.go:56
            Error:          Not equal: 
                            expected: 9
                            actual  : 8
            Test:           TestPlusFormat
            Messages:       not equal, collect = 8
--- FAIL: TestPlusFormat (0.00s)
FAIL
exit status 1
FAIL    testify-assert-example  0.002s

この後、別のtestifyパッケージを付けるので、このテストファイルはリネームしておきます。

$ mv calc_test.go _calc_test.go

requireパッケージ

assertパッケージの次は、requireパッケージについて。

require package

require - GoDoc

こちらは、assertパッケージと使い方が非常によく似ています。

assertと書いていたところをrequireにすると、ほぼ置き換えられます。以下は、先ほどのassertパッケージを使った
テストコードをrequireパッケージに置換してテスト関数名を少し変更したものです。
calc_require_test.go

package main

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

func TestPlusSimplyUsingRequire(t *testing.T) {
    result := Plus(1, 3)
    expect := 4

    require.Equal(t, expect, result)
}

func TestPlusRequireFailContinueUsingRequire(t *testing.T) {
    result1 := Plus(1, 3)
    expect1 := 4

    require.Equal(t, expect1, result1)

    result2 := Plus(1, 3)
    badExpect2 := 5 // miss

    require.Equal(t, badExpect2, result2)

    result3 := Plus(3, 5)
    expect3 := 8

    require.Equal(t, expect3, result3, "not equal")

    result4 := Plus(3, 5)
    badExpect4 := 9 // miss

    require.Equal(t, badExpect4, result4, "not equal")
}

func TestPlusFormatUsingRequire(t *testing.T) {
    result1 := Plus(1, 3)
    expect1 := 4

    require.Equalf(t, expect1, result1, "not equal, collect = %d", expect1)

    result2 := Plus(1, 3)
    badExpect2 := 5 // miss

    require.Equalf(t, badExpect2, result2, "not equal, collect = %d", 4)

    result3 := Plus(3, 5)
    expect3 := 8

    require.Equalf(t, expect3, result3, "not equal, collect = %d", expect3)

    result4 := Plus(3, 5)
    badExpect4 := 9 // miss

    require.Equalf(t, badExpect4, result4, "not equal, collect = %d", 8)
}

実行結果。

$ go test
--- FAIL: TestPlusRequireFailContinueUsingRequire (0.00s)
    calc_require_test.go:24: 
            Error Trace:    calc_require_test.go:24
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusRequireFailContinueUsingRequire
--- FAIL: TestPlusFormatUsingRequire (0.00s)
    calc_require_test.go:46: 
            Error Trace:    calc_require_test.go:46
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusFormatUsingRequire
            Messages:       not equal, collect = 4
FAIL
exit status 1
FAIL    testify-assert-example  0.002s


## -v付き
$ go test -v
=== RUN   TestPlusSimplyUsingRequire
--- PASS: TestPlusSimplyUsingRequire (0.00s)
=== RUN   TestPlusRequireFailContinueUsingRequire
    calc_require_test.go:24: 
            Error Trace:    calc_require_test.go:24
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusRequireFailContinueUsingRequire
--- FAIL: TestPlusRequireFailContinueUsingRequire (0.00s)
=== RUN   TestPlusFormatUsingRequire
    calc_require_test.go:46: 
            Error Trace:    calc_require_test.go:46
            Error:          Not equal: 
                            expected: 5
                            actual  : 4
            Test:           TestPlusFormatUsingRequire
            Messages:       not equal, collect = 4
--- FAIL: TestPlusFormatUsingRequire (0.00s)
FAIL
exit status 1
FAIL    testify-assert-example  0.003s

assertパッケージとの違いは、次の2つです。

  • 戻り値がない
  • アサーションに失敗すると、テスト関数が即終了する

なので、先ほどのassertパッケージを使ったテストコードと違い、アサーションに失敗した時点で後続のアサーションの呼び出しは
行われず、後続でエラーになるはずのアサーションの記録も残っていません。

これは、requireの中身を見るとわかりますが、内部的にはassertを呼び出し、アサーションに失敗するとtestingのT#FailNowを
呼び出すようになっているからです。

https://github.com/stretchr/testify/blob/v1.6.1/require/require.go

さらに言うと、requireパッケージはassertパッケージを使って自動生成されるようになっています。

https://github.com/stretchr/testify/blob/v1.6.1/require/require.go#L1-L4

https://github.com/stretchr/testify/blob/v1.6.1/_codegen/main.go#L31-L35

https://github.com/stretchr/testify/blob/v1.6.1/require/require.go.tmpl

なので、assertパッケージとほぼ同じことができる、というわけですね。

このファイルも、後続のテストのジャマにならないようにリネームしておきます。

$ mv calc_require_test.go _calc_require_test.go

suiteパッケージ

最後はsuiteパッケージです。suiteパッケージを使用すると、オブジェクト指向言語に似た形でテストの際にSetUpやTearDown
ということを行えます。

suite package

suite - GoDoc

これは、実際に書いていった方がいいでしょう。

最小構成は、こんな感じでしょうか。ここでは、最初に作ったcalc.goは無視しました…。
calc_suite_test.go

package main

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

type MyTestSuiteStruct struct {
    suite.Suite
}

func (suite *MyTestSuiteStruct) TestHello() {
    assert.Equal(suite.T(), 1, 1)
}

func TestFirstTestSuite(t *testing.T) {
    suite.Run(t, new(MyTestSuiteStruct))
}

とりあえず、suiteパッケージはimportするとして

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

最初に、suite#Suiteを埋め込んだ構造体を定義します。

type MyTestSuiteStruct struct {
    suite.Suite
}

テスト関数を作成し、suite#Runにtesting#Tと定義した構造体を初期化して渡します。

func TestFirstTestSuite(t *testing.T) {
    suite.Run(t, new(MyTestSuiteStruct))
}

あとは、定義した構造体をレシーバーとするメソッドを定義していきます。メソッド名は、Testで始めてください。

func (suite *MyTestSuiteStruct) TestHello() {
    assert.Equal(suite.T(), 1, 1)
}

アサーションは、今回はassertパッケージを使いました。assertを使うにはtesting#Tが必要ですが、これはsuite#Tで
取得できます。

func (*Suite) T

また、suite#Assertを使ってもOKです。同じ理屈で、suite#Requireも使えます。

func (*Suite) Assert

実行結果。

$ go test
PASS
ok      testify-assert-example  0.003s


## -v付き
$ go test -v
=== RUN   TestFirstTestSuite
=== RUN   TestFirstTestSuite/TestHello
--- PASS: TestFirstTestSuite (0.00s)
    --- PASS: TestFirstTestSuite/TestHello (0.00s)
PASS
ok      testify-assert-example  0.003s

テスト用のメソッドが、最初に定義したテスト関数のサブテストのような形態で実行されていますね。
これをまとめている単位が、テストスイートなのでしょうね。

ひとつ、テストメソッドを足してみましょう。これは、失敗するテストです。

func (suite *MyTestSuiteStruct) TestBool() {
    assert.True(suite.T(), false)
}

確認。

$ go test -v
=== RUN   TestFirstTestSuite
=== RUN   TestFirstTestSuite/TestBool
    calc_suite_test.go:18: 
            Error Trace:    calc_suite_test.go:18
            Error:          Should be true
            Test:           TestFirstTestSuite/TestBool
=== RUN   TestFirstTestSuite/TestHello
--- FAIL: TestFirstTestSuite (0.00s)
    --- FAIL: TestFirstTestSuite/TestBool (0.00s)
    --- PASS: TestFirstTestSuite/TestHello (0.00s)
FAIL
exit status 1
FAIL    testify-assert-example  0.003s

テスト関数内に、ひとつテストが追加されました。

この状態で、実行するテストメソッドを-runでコントロールすることもできるみたいですね。

$ go test -run TestFirstTestSuite/TestHello -v
=== RUN   TestFirstTestSuite
=== RUN   TestFirstTestSuite/TestHello
--- PASS: TestFirstTestSuite (0.00s)
    --- PASS: TestFirstTestSuite/TestHello (0.00s)
PASS
ok      testify-assert-example  0.003s

次に、テストの前後にメソッド呼び出しを挟んでみましょう。

テストの前後にメソッド呼び出しを追加する場合は、指定のインターフェースに定義されたメソッドを実装します。
このあたりですね。

どのインターフェースに定義されたメソッドを実装するかで、呼び出しタイミングが変わります。
なお、インターフェース名がメソッド名っぽいですが、実装すべきメソッド名と必ずしも一致していないので、どのメソッドを
実装すべきかはちゃんと定義を見た方がいいです…。

今回はテスト単位の前後に入れてみましょう。この目的の場合、SetupTestSuiteとBeforeTest、AfterTestとTearDownTestSuiteが
使えるのですが、今回はBeforeTestとAfterTestを使いましょう。

こんな感じです。

func (suite *MyTestSuiteStruct) BeforeTest(suiteName string, testName string) {
    suite.T().Log("BeforeTest!!")
}

func (suite *MyTestSuiteStruct) AfterTest(suiteName string, testName string) {
    suite.T().Log("AfterTest!!")
}

func (suite *MyTestSuiteStruct) TestHello() {
    suite.T().Log("run TestHello!!")
    assert.Equal(suite.T(), 1, 1)
}

func (suite *MyTestSuiteStruct) TestBool() {
    suite.T().Log("run TestBool!!")
    assert.True(suite.T(), false)
}

BeforeTest、AfterTestともに、引数にテストスイート名とテスト名を取ります。

今回は呼び出しタイミングがわかりやすいようにログ出力してみました。

確認。

$ go test -v
=== RUN   TestFirstTestSuite
=== RUN   TestFirstTestSuite/TestBool
    calc_suite_test.go:14: BeforeTest!!
    calc_suite_test.go:27: run TestBool!!
    calc_suite_test.go:28: 
            Error Trace:    calc_suite_test.go:28
            Error:          Should be true
            Test:           TestFirstTestSuite/TestBool
    calc_suite_test.go:18: AfterTest!!
=== RUN   TestFirstTestSuite/TestHello
    calc_suite_test.go:14: BeforeTest!!
    calc_suite_test.go:22: run TestHello!!
    calc_suite_test.go:18: AfterTest!!
--- FAIL: TestFirstTestSuite (0.00s)
    --- FAIL: TestFirstTestSuite/TestBool (0.00s)
    --- PASS: TestFirstTestSuite/TestHello (0.00s)
FAIL
exit status 1
FAIL    testify-assert-example  0.003s

こんな感じで、テスト前後に呼び出しが入っているのが確認できます。

また、テストスイートを定義する際の構造体は、埋め込んだsuite.Suite以外にもメンバーを持つことができます。

type SampleTestStruct struct {
    suite.Suite

    ExecutedTestMethodNames []string

    CallSetupSuiteCount    int
    CallSetupTestCount     int
    CallBeforeTestCounts   map[string]int
    runTestMethodNames     []string
    CallAfterTestCounts    map[string]int
    CallTearDownTestCount  int
    CallTearDownSuiteCount int
}

先ほど書いたサンプルとは別に、もうひとつテストスイートを起こしてみましょう。今回は、テストの前後に実行できるメソッドを
すべて実装します。
calc_suite2_test.go

package main

import (
    "encoding/json"
    "fmt"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/suite"
    "testing"
)

type SampleTestStruct struct {
    suite.Suite

    ExecutedTestMethodNames []string

    CallSetupSuiteCount    int
    CallSetupTestCount     int
    CallBeforeTestCounts   map[string]int
    runTestMethodNames     []string
    CallAfterTestCounts    map[string]int
    CallTearDownTestCount  int
    CallTearDownSuiteCount int
}

func (suite *SampleTestStruct) SetupSuite() {
    suite.T().Logf("===== SetupSuite =====")
    suite.CallSetupSuiteCount += 1
}

func (suite *SampleTestStruct) SetupTest() {
    suite.T().Logf("===== SetupTest =====")
    suite.CallSetupTestCount += 1
}

func (suite *SampleTestStruct) BeforeTest(suiteName string, testName string) {
    suite.T().Logf("===== BeforeTest suite[%s] test[%s] =====", suiteName, testName)

    if suite.CallBeforeTestCounts == nil {
        suite.CallBeforeTestCounts = make(map[string]int)
    }

    suite.CallBeforeTestCounts[suiteName+"/"+testName] += 1
}

func (suite *SampleTestStruct) AfterTest(suiteName string, testName string) {
    suite.T().Logf("===== AfterTest suite[%s] test[%s] =====", suiteName, testName)

    if suite.CallAfterTestCounts == nil {
        suite.CallAfterTestCounts = make(map[string]int)
    }

    suite.CallAfterTestCounts[suiteName+"/"+testName] += 1
}

func (suite *SampleTestStruct) TearDownTest() {
    suite.T().Logf("===== TearDownTest =====")
    suite.CallTearDownTestCount += 1
}

func (suite *SampleTestStruct) TearDownSuite() {
    suite.T().Logf("===== TearDownSuite =====")
    suite.CallTearDownSuiteCount += 1

    beforeTestCounts, _ := json.Marshal(suite.CallBeforeTestCounts)
    afterTestCounts, _ := json.Marshal(suite.CallAfterTestCounts)
    runTestMethodNames, _ := json.Marshal(suite.runTestMethodNames)

    fmt.Printf(`----------------------------------------------------------------------
  SetupSuiteCount = %d
  SetupTestCount = %d
  BeforeTestCounts = (
    %s
  )
  runTestMethods = %s
  AfterTestCounts = (
    %s
  )
  TearDownTestCount = %d
  TearDownSuiteCount = %d
----------------------------------------------------------------------
`,
        suite.CallSetupSuiteCount,
        suite.CallSetupTestCount,
        beforeTestCounts,
        runTestMethodNames,
        afterTestCounts,
        suite.CallTearDownTestCount,
        suite.CallTearDownSuiteCount,
    )
}

func (suite *SampleTestStruct) TestPlus() {
    suite.T().Logf("run %s", suite.T().Name())

    if suite.runTestMethodNames == nil {
        suite.runTestMethodNames = make([]string, 0)
    }

    suite.runTestMethodNames = append(suite.runTestMethodNames, suite.T().Name())

    assert.Equal(suite.T(), 1, 1)
}

func (suite *SampleTestStruct) TestBool() {
    suite.T().Logf("run %s", suite.T().Name())

    if suite.runTestMethodNames == nil {
        suite.runTestMethodNames = make([]string, 0)
    }

    suite.runTestMethodNames = append(suite.runTestMethodNames, suite.T().Name())

    assert.True(suite.T(), true)
}

func TestSuiteExample(t *testing.T) {
    suite.Run(t, new(SampleTestStruct))
}

構造体には、メソッド呼び出し時にそのタイミングに応じたカウンターを設け、記録していくようにしました。

type SampleTestStruct struct {
    suite.Suite

    ExecutedTestMethodNames []string

    CallSetupSuiteCount    int
    CallSetupTestCount     int
    CallBeforeTestCounts   map[string]int
    runTestMethodNames     []string
    CallAfterTestCounts    map[string]int
    CallTearDownTestCount  int
    CallTearDownSuiteCount int
}

結果。2つのテストスイートを実行しました。

$ go test -v
=== RUN   TestSuiteExample
    calc_suite2_test.go:26: ===== SetupSuite =====
=== RUN   TestSuiteExample/TestBool
    calc_suite2_test.go:31: ===== SetupTest =====
    calc_suite2_test.go:36: ===== BeforeTest suite[SampleTestStruct] test[TestBool] =====
    calc_suite2_test.go:105: run TestSuiteExample/TestBool
    calc_suite2_test.go:46: ===== AfterTest suite[SampleTestStruct] test[TestBool] =====
    calc_suite2_test.go:56: ===== TearDownTest =====
=== RUN   TestSuiteExample/TestPlus
    calc_suite2_test.go:31: ===== SetupTest =====
    calc_suite2_test.go:36: ===== BeforeTest suite[SampleTestStruct] test[TestPlus] =====
    calc_suite2_test.go:93: run TestSuiteExample/TestPlus
    calc_suite2_test.go:46: ===== AfterTest suite[SampleTestStruct] test[TestPlus] =====
    calc_suite2_test.go:56: ===== TearDownTest =====
=== CONT  TestSuiteExample
    calc_suite2_test.go:61: ===== TearDownSuite =====
----------------------------------------------------------------------
  SetupSuiteCount = 1
  SetupTestCount = 2
  BeforeTestCounts = (
    {"SampleTestStruct/TestBool":1,"SampleTestStruct/TestPlus":1}
  )
  runTestMethods = ["TestSuiteExample/TestBool","TestSuiteExample/TestPlus"]
  AfterTestCounts = (
    {"SampleTestStruct/TestBool":1,"SampleTestStruct/TestPlus":1}
  )
  TearDownTestCount = 2
  TearDownSuiteCount = 1
----------------------------------------------------------------------
--- PASS: TestSuiteExample (0.00s)
    --- PASS: TestSuiteExample/TestBool (0.00s)
    --- PASS: TestSuiteExample/TestPlus (0.00s)
=== RUN   TestFirstTestSuite
=== RUN   TestFirstTestSuite/TestBool
    calc_suite_test.go:14: BeforeTest!!
    calc_suite_test.go:27: run TestBool!!
    calc_suite_test.go:28: 
            Error Trace:    calc_suite_test.go:28
            Error:          Should be true
            Test:           TestFirstTestSuite/TestBool
    calc_suite_test.go:18: AfterTest!!
=== RUN   TestFirstTestSuite/TestHello
    calc_suite_test.go:14: BeforeTest!!
    calc_suite_test.go:22: run TestHello!!
    calc_suite_test.go:18: AfterTest!!
--- FAIL: TestFirstTestSuite (0.00s)
    --- FAIL: TestFirstTestSuite/TestBool (0.00s)
    --- PASS: TestFirstTestSuite/TestHello (0.00s)
FAIL
exit status 1
FAIL    testify-assert-example  0.004s

追加した方のテストスイートの表示は、こちらですね。

=== RUN   TestSuiteExample
    calc_suite2_test.go:26: ===== SetupSuite =====
=== RUN   TestSuiteExample/TestBool
    calc_suite2_test.go:31: ===== SetupTest =====
    calc_suite2_test.go:36: ===== BeforeTest suite[SampleTestStruct] test[TestBool] =====
    calc_suite2_test.go:105: run TestSuiteExample/TestBool
    calc_suite2_test.go:46: ===== AfterTest suite[SampleTestStruct] test[TestBool] =====
    calc_suite2_test.go:56: ===== TearDownTest =====
=== RUN   TestSuiteExample/TestPlus
    calc_suite2_test.go:31: ===== SetupTest =====
    calc_suite2_test.go:36: ===== BeforeTest suite[SampleTestStruct] test[TestPlus] =====
    calc_suite2_test.go:93: run TestSuiteExample/TestPlus
    calc_suite2_test.go:46: ===== AfterTest suite[SampleTestStruct] test[TestPlus] =====
    calc_suite2_test.go:56: ===== TearDownTest =====
=== CONT  TestSuiteExample
    calc_suite2_test.go:61: ===== TearDownSuite =====
----------------------------------------------------------------------
  SetupSuiteCount = 1
  SetupTestCount = 2
  BeforeTestCounts = (
    {"SampleTestStruct/TestBool":1,"SampleTestStruct/TestPlus":1}
  )
  runTestMethods = ["TestSuiteExample/TestBool","TestSuiteExample/TestPlus"]
  AfterTestCounts = (
    {"SampleTestStruct/TestBool":1,"SampleTestStruct/TestPlus":1}
  )
  TearDownTestCount = 2
  TearDownSuiteCount = 1
----------------------------------------------------------------------

これで、テストの実行タイミングが確認できると思います。

一応、メソッドと実行タイミングの対応を書くと、以下のようになります。

メソッド名 実行タイミング
SetupSuite テストスイートの実行前
SetupTest (テストスイート内の)各テストメソッドの実行前
BeforeTest (テストスイート内の)各テストメソッドの実行前
AfterTest (テストスイート内の)各テストメソッドの実行後
TearDownTest (テストスイート内の)各テストメソッドの実行後
TearDownSuite テストスイートの実行後

SetupTestとBeforeTest、AfterTestとTearDownTestの違いは、引数の有無と考えればよさそうです。

このあたりの、テストメソッドと前後に入れ込むメソッドの実行タイミングの定義は以下を確認すると良いでしょう。

https://github.com/stretchr/testify/blob/v1.6.1/suite/suite.go#L112-L176

また、テストスイートのグルーピングの単位がsuite#Suiteを埋め込んだ構造体であることも確認できましたね。

あと、suiteパッケージの使い方としては、こちらを見てもよいでしょう。

https://github.com/stretchr/testify/blob/v1.6.1/suite/suite_test.go

まとめ

testifyのassert(とrequire)、suiteに関してちょっと試してみました。

まだGo自体に慣れないので、suiteの使い方は特に手間取ったのですが、testifyのソースコードもそれほど大きくなかったので
Goを読む勉強としても良かったです。