Goのテストを楽に書くためのTipsです。
読みやすさもアップするのでウチのGoプロジェクトでは頻繁に使っています。
1. テスト対象は外部パッケージとして扱う
テストコードがそのまま簡易ドキュメントになったら素敵ですよね。
外部パッケージとしてテストを書くことで実現できます。
例えばutil.go
にこんな実装があったとします。
package util
import (
"fmt"
)
const prefix = "Hello"
// 名前を受け取って挨拶を返却。
func Hello(name string) string {
return fmt.Sprintf("%s %s", prefix, name)
}
これをテストするには同じディレクトリにutil_test.go
というファイルを作ると思います。
通常はこうです。
package util
import (
"testing"
)
func TestHello(t *testing.T) {
exp := "Hello Yamada-san"
// ここで関数をコール
got := Hello("Yamada-san")
if exp != got {
t.Errorf("expected %s, got %s", exp, got)
}
}
これは一見すると何の問題も無いんですが、このパッケージを外部から利用する時にHello("Yamada-san")
をコピペすると動きません。util.Hello("Yamada-san")
ならOKです。エクスポートしている関数が僅かなら大した手間では無いのですが数が増えるのに比例して手間も増えます。
ということで、パッケージの末尾に_test
を付けてみましょう。
util_test
になりますね、外部パッケージとしてテストが書けます。
package util_test // コレで外部パッケージになる
import (
"testing"
"github.com/tomodian/whatever/util"
)
func TestHello(t *testing.T) {
exp := "Hello Yamada-san"
got := util.Hello("Yamada-san") // 外部パッケージとしてコール
if exp != got {
t.Errorf("expected %s, got %s", exp, got)
}
}
コピペ性能がグッと高まりましたね!
しかし、テストコードが外部パッケージになったので、エクスポートされていない実装が見えなくなります。
こういう場合は内部パッケージのテストだということを明示してutil_internal_test.go
というファイルを作ってみましょう。
package util // 同一の名前空間
import (
"testing"
)
func TestInternalPrefix(t *testing.T) {
// 内部の定数をチェック
if prefix != "Hello" {
t.Errorf("wrong prefix")
}
}
ファイル構成はこんな感じになりました。
- util.go → 実体
- util_test.go → 外部パッケージとしてテスト
- util_internal_test.go → 内部テスト
2. パターンテストは配列で
これはGoに限らず、どんなプログラミング言語にも使えるテクニックです。
関数に色々な値を渡して出力結果をチェックしたいときは、パターンを配列にすると楽です。
例えばstringutil
というパッケージがあり、文字列に含まれる数値を拾ってint
を返却する関数を作る場合はこんな実装になると思います。
package stringutil
import (
"regexp"
"strconv"
)
// 文字列から数値を抜き出してint型で返却。小数点は考慮しない。
// エラーの場合はゼロ (Default Value)にフォールバック。
func Numeric(in string) int {
re := regexp.MustCompile(`[^0-9]+`)
got, err := strconv.Atoi(re.ReplaceAllString(in, ""))
if err != nil {
return 0
}
return got
}
んで、これに色々な値を渡すときに配列を使います。
package stringutil_test
import (
"testing"
"api/app/utils/stringutil"
)
func TestNumeric(t *testing.T) {
patterns := []struct {
exp int // 期待値 (expectation)
given string // 入力値 (given input)
}{
// パターンを把握して追加・削除を行うのが楽になる
{10, "010"},
{100, "100"},
{100, "Hello100"},
{100, "Hello100🍎"},
{0, "Hello"},
}
for idx, p := range patterns {
got := stringutil.Numeric(p.given)
if p.exp != got {
t.Errorf("Case(%d) expected %d, got %d", idx, p.exp, got)
}
}
}
3. Success / Failケースをスッキリと書く
エラーを返却して欲しいようなテストケースを書くとき。
例えばさっきの関数は問題が起きたらゼロにフォールバックするようになっていましたが、これだと本当にゼロなのかエラーが握り潰されたのかが分からないので素直にエラーを返却するようにリファクタリングしてみます。
package stringutil
import (
"regexp"
"strconv"
)
func Numeric(in string) (int, error) { // 返却内容が増えた
re := regexp.MustCompile(`[^0-9]+`)
got, err := strconv.Atoi(re.ReplaceAllString(in, ""))
if err != nil {
return 0, err // エラーが起きたら素直に返却
}
return got, nil
}
さて、こうなるとSuccessケースとFailケースを考慮したテストが要りますね。
先ほどのTestNumeric
をTestNumericSucceeds
とTestNumericFails
に分割してみましょうか。
package stringutil_test
import (
"testing"
"api/app/utils/stringutil"
)
func TestNumericSucceeds(t *testing.T) {
patterns := []struct {
exp int // 期待値 (expectation)
given string // 入力値 (given input)
}{
{100, "Hello100🍎"},
}
for idx, p := range patterns {
got, err := stringutil.Numeric(p.given)
if err != nil {
t.Errorf("Case(%d) failed, error %s", idx, err)
continue
}
if p.exp != got {
t.Errorf("Case(%d) expected %d, got %d", idx, p.exp, got)
}
}
}
func TestNumericFails(t *testing.T) {
patterns := []struct {
given string // 入力値 (given input)
}{
{"エラーを期待💣"},
}
for idx, p := range patterns {
got, err := stringutil.Numeric(p.given)
if err == nil || got != 0 {
t.Errorf("Case(%d) should fail", idx)
}
}
}
うーむ、テストコードが冗長になってしまいました。
まずテスト対象がstringutil.Numeric
なのに*Succeeds
と*Fails
という2つのテストケースに分割されています。一見問題ないように見えるんですが、patterns
構造体が2つあるので、テスト対象が増えるとメンテも増えそうです。
そもそもstringutil.Numeric
をテストしたいだけなのにpatterns
構造体を考慮すること自体が無駄な気がします。
こんな時はケースごとに無名関数で括るとスッキリ書けます。
package stringutil_test
import (
"testing"
"api/app/utils/stringutil"
)
func TestNumeric(t *testing.T) { // Numeric関数のテストはここに全て詰め込む
// 構造体は1つ
type pattern struct {
exp int // 期待値 (expectation)
given string // 入力値 (given input)
}
// Successケース
func() {
patterns := []pattern{
{10, "010"},
{100, "100"},
{100, "Hello100"},
{100, "Hello100🍎"},
}
for idx, p := range patterns {
got, err := stringutil.Numeric(p.given)
if err != nil {
t.Errorf("Case(%d) failed, error %s", idx, err)
continue
}
if p.exp != got {
t.Errorf("Case(%d) expected %d, got %d", idx, p.exp, got)
}
}
}()
// Failケース
func() {
patterns := []pattern{
{0, "エラーを期待"},
{0, "これもNG💀"},
}
for idx, p := range patterns {
// Failケースはerrorの返却有無だけをチェックすればOK
if _, err := stringutil.Numeric(p.given); err == nil {
t.Errorf("Case(%d) must fail")
}
}
}()
}
だいぶ楽になりましたね! 1つのpattern
構造体でどちらのケースもカバーし、無名関数で括って変数のスコープを閉じたことでほぼ同じコードでテストが書けるようになりました。
4. テストに絵文字を入れる
上記のテストには🍎💀が入ってますが、テストコードに日頃から絵文字を入れておくと思わぬバグを検出することができます。
絵文字はUTF-8で28バイトなので、文字数を厳密にカウントしなければならないとき、例えばRDBのカラム長が絡むときはテスト時にエラーを検出できます。
まとめと宣伝
Goは素晴らしい言語ですが、コードが冗長になりがちなのでなるべくリファクタリング工数が増えないように工夫しています。
今月出たばかりの弊社のSaaS「チームで使える在庫管理アプリ Tana」のサーバサイドは全てGoで動いていて、テストコードには今回ご紹介したテクニックが頻繁に使われています。