2025/10/06

actually の FailNow をガードで書けるようにした

テスト関数の構成

なんらかのテストを書くとき、おおむね以下のような構造になっていると思います。

func TestSome(t *testing.T) {
    // prepare to test action
    db, err := database.instance("user")
    require.NoError(t, err)
    db.insert(model.User{id: 1})

    // test action
    res, err := someAction()

    // confirm
    assert.NoError(t, err)
    assert.Equal(t, expect, res)
}

テスト実行のための事前準備と、本テスト実行と、実行結果の確認というステップがあるわけです。

testify だと require か assert を使い分ける

テスト実行のための事前準備のときに、なんらか意図しない挙動があったときは、require でテストして、失敗したらその場で実行を止めるようにしているかと思います。

また、テスト実行結果の確認などは、失敗しても継続するテストを実行するように assert を使っていると思います。

testify などのフレームワークを使わない場合でも、t.Errort.Fatal をなどを同様に使い分けていることと思います。

actually の FailNow

自分が書いている actually でも、FailNow というメソッドがあり、それを呼ぶと require 相当のテストになって、失敗したらその場で実行が止まるようにしていました。

actually.Got(v).FailNow().True(t)

ただ、テストケースごとに FailNow を書くのは非常に面倒です。そこで FailNowOn というスイッチも用意していました。

func Test(t *testing.T) {
    // turn on to fail right now
    actually.FailNowOn(t)
    actually.Got(something).Nil(t)                    // Fail Now
    actually.Got(something).Expect(something).Same(t) // Fail Now
}

しかし、このスイッチの実装内部では、フラグに環境変数を利用しているため並列実行できなくなるという弱点がありました。これは実装当初から気づいていたのですが、他のアイデアがなく、このような形になっていました(当時は自分も並列実行するようなものを書いていなかったため)。

というわけで、今回この FailNow を冒頭で書いたユースケースも踏まえて再実装しました。

actually の Guard

まず、単体の FailNow は同じです。テスト実行ごとに FailNow を明示できます。これは変わってません。

actually.Got(v).FailNow().True(t)

そして、ガードっぽい FailNow を追加しました。

actually.FailNow(func() {
    actually.Got(false).True(t) // この失敗で実行は止まる
    actually.Got(true).True(t)  // このテストは実行されない
})

actually.FailNow に func を渡して、その func の内部で実行される actually のアサーションはすべて FailNow 状態になります(こけたらその場で実行が止まる)。

これの何がうれしいかというと、まず、actually 単体で呼べるので require/assert のように 2 パッケージ読まなくていいし、go 自身の t.FailNow とワーディングも同じなので覚えることが少なくて済みます。また、ユースケースで書いたように、Fail now したいのはテストの事前準備段階なので、ガードで囲まれていればそこは事前準備をしているのだなと明示的に見えるようになるわけです(そうじゃないときも使えますもちろん)。あと、内部実装的には若干飛び道具を使ってますが、groutine safe にもなっています。

この対応は、actually v0.37.0 から入っています。やったぜ

サイト内検索