testify の require を使う

golang.org/stretchr/testifyrequireパッケージを使用する。 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番目のテストは、diverror を返さないことをチェックしている。 そして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 と全く同じで、assertrequire に変えるだけでよい。

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

test2testYYY は実行されている。

補足

ちなみにどうしてこのような挙動になるかというと、require 内のそれぞれの関数内で assert によるアサーションを実行し、失敗したら (*testing.T).FailNow() を呼び出しテストを停止させているから。(つまりこの記事の1番目の解決方法と同じようなことを中でやっている)

github.com