golang.org/stretchr/testify
の require
パッケージを使用する。
assert
と同様のテストを行いつつ、テストに失敗した場合は後続のテストの実行を止めることができる。
Package require implements the same assertions as the
assert
package but stops test execution when a test fails. (require パッケージは、assert パッケージと同一のアサーションを実装し、かつテストに失敗したときはテストの実行を停止します)
なお testify/assert
については過去に以下で記事を書いているのでよろしければ。
例えば割り算を行う関数を持つプログラムを書いたとする。
package main import ( "errors" "fmt" "log" ) func main() { a := 8 b := 2 r, err := div(a, b) if err != nil { log.Fatal(err) } fmt.Printf("%d / %d = %d\n", r.dividend, r.divisor, r.quotient) } type DivResult struct { dividend int divisor int quotient int } func div(a, b int) (*DivResult, error) { if b == 0 { return nil, errors.New("division by zero") } return &DivResult{a, b, a / b}, nil }
ここで関数 div
は、割り算の結果を DivResult
という構造体に格納して返すようになっている。
また除数が 0
の場合は結果自体は nil
とし、error
を別途返すようにしている。
さて、この関数 div
のテストを書いてみる。
testify/assert
だけを使うと以下のようになるかもしれない。
package main import ( "testing" "github.com/stretchr/testify/assert" ) func TestDiv(t *testing.T) { got, err := div(8, 4) assert.NoError(t, err) assert.Equal(t, 2, got.quotient) }
1番目のテストは、div
が error
を返さないことをチェックしている。
そして2番めのテストは、結果の DivResult
に含まれる quotient
が正しいことをチェックしている。
このテストは現状では PASS するし、何の問題もなさそうだ。
しかし、もしうっかり div
関数の実装を以下のようにしてしまったらどうだろうか?
func div(a, b int) (*DivResult, error) { if b != 0 { // <<--- うっかり条件を逆にしてしまった return nil, errors.New("division by zero") } return &DivResult{a, b, a / b}, nil }
もちろんテストは通らなくなる。それどころか panic まで発生してしまう。
=== RUN TestDiv main_test.go:11: Error Trace: /tmp/testify_require/main_test.go:11 Error: Received unexpected error: division by zero Test: TestDiv --- FAIL: TestDiv (0.00s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4b0267c] goroutine 19 [running]: testing.tRunner.func1.2({0x4b7d420, 0x4cd5050}) /Users/egawata/ghq/github.com/golang/go/src/testing/testing.go:1545 +0x24a testing.tRunner.func1() /Users/egawata/ghq/github.com/golang/go/src/testing/testing.go:1548 +0x397 panic({0x4b7d420?, 0x4cd5050?}) /Users/egawata/ghq/github.com/golang/go/src/runtime/panic.go:612 +0x132 testify_assert.TestDiv(0x0?) /tmp/testify_require/main_test.go:12 +0x5c testing.tRunner(0xc0000831e0, 0x4badc80) /Users/egawata/ghq/github.com/golang/go/src/testing/testing.go:1595 +0xff created by testing.(*T).Run in goroutine 1 /Users/egawata/ghq/github.com/golang/go/src/testing/testing.go:1648 +0x3ad FAIL testify_assert 0.276s FAIL
関数の実装が間違っているのだからテストが FAIL するのは構わない。 しかし以下の2点で問題がある。
- そもそもテスト実行中に panic を起こしたくない
- 1番目のテストが
error
を返した時点で2番目のテストを行う意味がなくなり、無駄なテストを実行しているdiv
関数は1番目の戻り値で計算結果を返すが、エラー発生時には値に意味がないという意図のnil
になるから
解決方法としてまず考えられるのは、1番目のテストが通ったことを確認してから後続のテストを行うというもの。
assert
のテスト関数は、テストが成功/失敗すると、それぞれ true
/ false
を返すので、その結果を利用できる。
func TestDiv(t *testing.T) { got, err := div(8, 4) if !assert.NoError(t, err) { // <<--- assert.NoError の結果をチェック t.Fatal(err) } assert.Equal(t, 2, got.quotient) }
これで1番目のテストに失敗したら2番目に進まないようにできる。
しかし冗長さがあるのは否めない。そもそも testify/assert
を使いたい理由って
if (条件) {
t.Fatal(err)
}
みたいな冗長な記述を簡略化したかったということなのではないか。これでは元の木阿彌だ。
そこで testify
に同梱されている require
パッケージを使う。
使い方は testify/assert
と全く同じで、assert
を require
に変えるだけでよい。
import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // <-- 追加 ) func TestDiv(t *testing.T) { got, err := div(8, 4) require.NoError(t, err) // <-- require に変更 assert.Equal(t, 2, got.quotient) }
=== RUN TestDiv main_test.go:12: Error Trace: /tmp/testify_require/main_test.go:12 Error: Received unexpected error: division by zero Test: TestDiv --- FAIL: TestDiv (0.00s) FAIL FAIL testify_assert 0.249s FAIL
require.NoError
のテストが失敗した時点で後続のテストがキャンセルされる。
停止されるテストの範囲
ここで FAIL 時に停止されるのは、同一テスト関数内の後続テスト。他のテスト関数は影響を受けない。
TestXXX
のような関数内でrequire
パッケージによりアサーション関数が実行された場合は、TestXXX
内の後続のテストは行われない。- 他の
TestYYY
などのテストは引き続き実行される。
- 他の
t.Run()
などでサブテストが作成され、その中でテスト関数が実行されている場合は、そのサブテスト内のテストのみが停止となる。
func TestDiv(t *testing.T) { t.Run("test1", func(t *testing.T) { got, err := div(8, 4) require.NoError(t, err) // <-- これが fail するとする assert.Equal(t, 2, got.quotient) // <-- 実行されない }) t.Run("test2", func(t *testing.T) { assert.Equal(t, 3, 1+2) // <-- 実行される }) } func TestYYY(t *testing.T) { assert.Equal(t, 8, 2*4) // <-- 実行される }
=== RUN TestDiv === RUN TestDiv/test1 main_test.go:13: Error Trace: /tmp/testify_require/main_test.go:13 Error: Received unexpected error: division by zero Test: TestDiv/test1 === RUN TestDiv/test2 --- FAIL: TestDiv (0.00s) --- FAIL: TestDiv/test1 (0.00s) --- PASS: TestDiv/test2 (0.00s) === RUN TestYYY --- PASS: TestYYY (0.00s) FAIL FAIL testify_assert 0.317s FAIL
test2
と testYYY
は実行されている。
補足
ちなみにどうしてこのような挙動になるかというと、require
内のそれぞれの関数内で assert
によるアサーションを実行し、失敗したら (*testing.T).FailNow()
を呼び出しテストを停止させているから。(つまりこの記事の1番目の解決方法と同じようなことを中でやっている)