こちらはGo 5 Advent Calendar 2020の5日目の記事です。
こんにちは、うえちょこ(uechoco)です。
DeNAから事業統合で株式会社Mobility Technologiesに転籍しまして、バックエンドエンジニアをしております。
ええ、あの「GO」という名前のタクシー配車アプリを「Go」で作っている会社です 。(これが言いたかったw
最近、プルリクのコメントで今回紹介するテクニックの1つをつぶやいてみたら、意外と知られていなかったので、書いてみようかなと思った内容です。
まずはテストに関するおさらい
_test.go
ファイル
_test.go
というsuffixのファイルにすると、これらのファイルに書かれたコードを通常のビルドから除外してくれます。
余談ですが、 Test関数以外にも、Benchmark関数だったり Example関数 だったりもこのファイルに書きます。
_test
パッケージ
Goでは1つのディレクトリ内のコードは1つのパッケージに所属するようになっていますが、例外として _test
パッケージは置くことができます(例: fmt
パッケージに対する fmt_test
パッケージ)。
別のパッケージにすることで、テスト対象のパッケージを使う(importする)側に立って関数を実行するので、公開されている関数が期待通りに動くかどうかを確認することができます。
一方で _test
パッケージを用いると、非公開な関数や非公開なフィールド変数にテストコード側からアクセスすることができなくなります。非公開なコードをテストで使いたい、テストしておきたいときに困ります。
そんなときによく見かけるテクニックが以下の2つです。
テクニック1: export_test.go
を使う
export_test.go
というファイルを置いて、テストコードのための特別な実装をしておくというテクニックです。これは golang本体でも頻出です。
いくつかの記事で紹介されています。
- Go Fridayこぼれ話:非公開(unexported)な機能を使ったテスト #golang | メルカリエンジニアリング
- Golang Trick: Export unexport method for test | by lysu | Medium
非公開関数のテストコードを書く例
golang本体の事例を簡単に書いておきます。
-
https://github.com/golang/go/blob/master/src/fmt/print.go
- parsenum()関数は小文字始まりなので、非公開関数です。
func parsenum(s string, start, end int) (num int, isnum bool, newi int) {
// 省略
}
-
https://github.com/golang/go/blob/master/src/fmt/export_test.go
- テスト対象と同じパッケージ名で、非公開関数の公開エイリアスを定義しています。
-
_test.go
ファイルなので、テスト用ビルドでのみ使用できます。 - こうすることで、テスト用ビルドのときだけ拡張され、かつテストコードは公開・非公開問わず
_test
パッケージに書くことができます。
package fmt
var Parsenum = parsenum
-
https://github.com/golang/go/blob/master/src/fmt/fmt_test.go
- TestParsenum は Parsenum() 関数を呼び出して parsenum()関数のテストとしています。
func TestParsenum(t *testing.T) {
testCases := []struct {
// 省略
}{
// 省略
}
for _, tt := range testCases {
num, isnum, newi := Parsenum(tt.s, tt.start, tt.end)
// 省略
}
}
その他の例
export_test.go
は、非公開関数の公開エイリアスの定義だけではなく、テスト用にごにょりたい用の関数をおいておく場所としても利用されます。例えば非公開フィールドにアクセスするための関数を生やしたりすることにも使われています。
テクニック2: _internal_test.go
を使う
こちらは先程のテクニックよりマイナーかもしれません。
テスト対象と同じ( _test
ではない )パッケージの中にテストコードを書いているという目印として、ファイル名の末尾に _internal_test.go
を付けようというルールです。
いくつかの記事で紹介されています。
- Next level Go testing - Tit Petric
- 5 simple tips and tricks for writing unit tests in #golang | by Mat Ryer | Medium
golang本体では1箇所だけ使用されています。
-
https://github.com/golang/go/blob/master/src/net/http/transport_internal_test.go
-
_test
パッケージののテストコードもあります: https://github.com/golang/go/blob/master/src/net/http/transport_test.go
-
package http
// 省略
func TestTransportPersistConnReadLoopEOF(t *testing.T) {
// 省略
tr := new(Transport)
// 省略
pc, err := tr.getConn(treq, cm)
// 省略
}
// 省略
こちらは同じパッケージ内にテストコードを書くので、 export_test.go
と違って公開用エイリアスの定義を挟むことはありません。直接テストコードの中から非公開関数を呼び出すことができます。私はテスト用に便利関数を生やしたりといったケースにあまり遭遇してこなかったので、いまのところは export_test.go
よりは _internal_test.go
をよく使います。
余談ですが、とあるgoのOSSのissueで、「内部テストを書きたいから _internal_test.go
を使っていこう」というissueに対して「マイナーなルールだから受け入れられない」とメンテナが判断してcloseされてしまっていたケースを見かけました。
ところでlinterで怒られないの?
golangci-lintでサポートされているlinterの1つにtestpackageがあります。
これはテストが _test
パッケージ以外に書かれているときに教えてくれるlinterです。
このlinterのデフォルト挙動では、 XXX_internal_test.go
や export_test.go
といったファイル名の場合はスキップするように設定されています。今回紹介したテクニックを使っても nolintコメントを記述する必要はありません。