🤖

genericsの練習用にgoのテストでerrorやcleanup処理を含むfactory関数を手軽に使うためのライブラリを書いてました

2022/07/28に公開

genericsの練習用にお遊びで作ったのですが、意外と便利そうなので紹介してみます。
goのテストコードを書くときに、主にファクトリー関数などで発生するerror値やcleanup処理のハンドリングが面倒なことがあります。それをいい感じに扱えるライブラリです[1]

https://github.com/podhmo/or

使い方

使い方はor.Fatal()を使うのが基本です。この関数は func(...) (T, error) のような関数の利用を func(...) Tの関数であったかのように扱ってくれます[2]。

type DB struct {}
func NewDB() (*DB, error) { return &DB{}, nil}

func TestIt(t *testing.T){
	db := or.Fatal(NewDB())(t)
}

少し呼び出し方が特殊で or.Fatal(t, NewDB()) などではないのは、goの多値の仕様とgenericsの仕様による制限です。個人的にはパッケージ名にorとつけたことで「もしくは失敗」というようなニュアンスで読むことができて気に入っています。

何が嬉しいか?

個人的に感じる嬉しさは以下2つです

  • setup部分のコードが減ることで、実際のテストコード部分に集中ができる
  • 多段に重なったerror対応などから開放される

読むときに楽になりそうなのが前者で、書くときに楽になるのが後者というイメージです。
多段に重なったerrorの対応と言うのは例えば以下のようなものです。getFoo()でエラーになってもgetBar()でエラーになっても大丈夫です。

func TestFatal(t *testing.T) {
	foo := or.Fatal(getFoo())(t)
	bar := or.Fatal(getBar(foo))(t)

	_ = bar // doSomething
}

ここで、getFoo()は func() (*Foo, error) という関数で getBarはFooを受け取る func(*Foo) (*Bar, error)のような関数です。

このような依存部分の値の生成の連鎖の途中部分でエラーになった場合にはそこでテストが止まって失敗してくれます(実際にはtesting.T.Fatal()が呼ばれる)。

例えば以下のような形で失敗します。

$ go test
--- FAIL: TestFatal (0.00s)
    example_test.go:24: accessing fail (*main.Bar): hmm
--- FAIL: TestFatalWithCleanup (0.00s)
    example_test.go:31: accessing fail (*main.Boo): hmm
FAIL
exit status 1
FAIL	github.com/podhmo/or/testdata	0.003s

Fatal()の親戚

or.Fatal() の他にcleanup関数に対応した or.FatalWithCleanup() という関数も用意しています[3]。こちらも内部で testing.T.Cleanup()を呼んであげているので同様の感覚で使えます。

func TestFatalWithCleanup(t *testing.T) {
	foo := or.Fatal(getFoo())(t)
	boo := or.FatalWithCleanup(getBoo(foo))(t)

	_ = boo // doSomething
}

その他細々と関数が用意されていますが、基本的にはこの2つを把握しておけば大丈夫だと思います。

本当にこのパッケージは必要?

と、ここまで作ったパッケージの宣伝をしてきたわけですが、本当にこのパッケージが必要なんだろうか?と疑問に湧く人もいると思います。実際には丁寧にテスト用のパッケージを書いてあげて広義の意味でのfixtureを担うfactory関数だったり、テストヘルパーだったりを書いてあげると不要になります。

わかり易い例で言えば、net/http に対する net/http/httptest でしょうか? httptest.NewRequest() はエラー値を返さないですよね。そんな感じで、今回の例に関しても別途テスト用のfactory関数を書いてあげれば不要になります。

[1]: いわゆるrustで言えば ! のような、あるいはErrorモナドのような何か
[2]: たまにパッケージに存在する Must() のような関数ですね
[3]: 名付けに自信がないので、もっと良い名前を募集していたりします

Discussion