golangで非公開関数をテストするためのよくあるテクニック

こちらは[Go 5 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go5)の5日目の記事です。

こんにちは、うえちょこ(uechoco)です。
DeNAから事業統合で株式会社Mobility Technologiesに転籍しまして、バックエンドエンジニアをしております。
**ええ、あの「GO」という名前のタクシー配車アプリを「Go」で作っている会社です** 。（これが言いたかったｗ

最近、プルリクのコメントで今回紹介するテクニックの１つをつぶやいてみたら、意外と知られていなかったので、書いてみようかなと思った内容です。

# まずはテストに関するおさらい

## `_test.go` ファイル

https://tip.golang.org/pkg/testing/

`_test.go` というsuffixのファイルにすると、これらのファイルに書かれたコードを通常のビルドから除外してくれます。

余談ですが、 Test関数以外にも、Benchmark関数だったり [Example関数](https://blog.golang.org/examples) だったりもこのファイルに書きます。

## `_test` パッケージ

https://tip.golang.org/cmd/go/#hdr-Test_packages

Goでは１つのディレクトリ内のコードは1つのパッケージに所属するようになっていますが、例外として `_test` パッケージは置くことができます(例: `fmt` パッケージに対する `fmt_test` パッケージ)。
別のパッケージにすることで、テスト対象のパッケージを使う（importする）側に立って関数を実行するので、公開されている関数が期待通りに動くかどうかを確認することができます。

一方で `_test` パッケージを用いると、非公開な関数や非公開なフィールド変数にテストコード側からアクセスすることができなくなります。非公開なコードをテストで使いたい、テストしておきたいときに困ります。

そんなときによく見かけるテクニックが以下の2つです。

# テクニック1: `export_test.go` を使う

 `export_test.go` というファイルを置いて、テストコードのための特別な実装をしておくというテクニックです。これは golang本体でも頻出です。

いくつかの記事で紹介されています。

* [Go Fridayこぼれ話：非公開（unexported）な機能を使ったテスト \#golang \| メルカリエンジニアリング](https://engineering.mercari.com/blog/entry/2018-08-08-080000/)
* [Golang Trick: Export unexport method for test \| by lysu \| Medium](https://medium.com/@robiplus/golang-trick-export-for-test-aa16cbd7b8cd)

## 非公開関数のテストコードを書く例

golang本体の事例を簡単に書いておきます。

* https://github.com/golang/go/blob/master/src/fmt/print.go
    * parsenum()関数は小文字始まりなので、非公開関数です。

```go
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` パッケージに書くことができます。

```go
package fmt

var Parsenum = parsenum
```

* https://github.com/golang/go/blob/master/src/fmt/fmt_test.go
    * TestParsenum は Parsenum() 関数を呼び出して parsenum()関数のテストとしています。

```go
func TestParsenum(t *testing.T) {
	testCases := []struct {
		// 省略
	}{
		// 省略
	}
	for _, tt := range testCases {
		num, isnum, newi := Parsenum(tt.s, tt.start, tt.end)
		// 省略
	}
}
```

## その他の例

`export_test.go` は、非公開関数の公開エイリアスの定義だけではなく、テスト用にごにょりたい用の関数をおいておく場所としても利用されます。例えば非公開フィールドにアクセスするための関数を生やしたりすることにも使われています。

* golang本体の例: https://github.com/golang/go/blob/master/src/time/export_test.go

# テクニック2: `_internal_test.go` を使う

こちらは先程のテクニックよりマイナーかもしれません。
テスト対象と同じ( `_test` ではない )パッケージの中にテストコードを書いているという目印として、ファイル名の末尾に `_internal_test.go` を付けようというルールです。

いくつかの記事で紹介されています。

* [Next level Go testing \- Tit Petric](https://scene-si.org/2019/04/15/next-level-go-testing/#public-vs-private-tests-apis)
* [5 simple tips and tricks for writing unit tests in \#golang \| by Mat Ryer \| Medium](https://medium.com/@matryer/5-simple-tips-and-tricks-for-writing-unit-tests-in-golang-619653f90742)

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

```go:transport_internal_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](https://github.com/maratori/testpackage)があります。
これはテストが `_test` パッケージ以外に書かれているときに教えてくれるlinterです。

このlinterのデフォルト挙動では、 `XXX_internal_test.go` や `export_test.go` といったファイル名の場合はスキップするように設定されています。今回紹介したテクニックを使っても nolintコメントを記述する必要はありません。

# おわり