golang.org/stretchr/testify の requireパッケージを使用する。
assert と同様のテストを行いつつ、テストに失敗した場合は後続のテストの実行を止めることができる。
Package require implements the same assertions as the
assertpackage 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番目の解決方法と同じようなことを中でやっている)