- 辻大志郎
- 約 12,900 文字
- 8,000 View
目次
- はじめに
- Tips
- テストがしたい
- テストの失敗をレポートしたい
- テストをスキップしたい
- テストを並列に実施したい
- テストの前処理や後処理を実施したい
- カバレッジを取得したい
- 環境変数を設定したい
- あるディレクトリ配下のテストをすべて実施したい
- 一部のテストケースのみ実施したい
- サブテストの一部のみ実施したい
- テストのキャッシュを削除したい
- 一時ディレクトリを作成したい
- テストコードの雛形を楽に作りたい
- 構造体、マップやスライスの比較を実施したい
- APIサーバにアクセスするテストをしたい
- APIサーバのハンドラのテストをしたい
- データ競合をテストで検知したい
- テストデータを置いておきたい
- テストにヘルパー関数を使いたい
- ブラックボックステストをしたい
- 用途(単体・インテグレーション)などによってテストを切り替えたい
- モックを使ってテストをしたい
- コンテナを扱いたい
- ゴルーチンリークを検出したい
- まとめ
- 参考
春の入門祭り2025 13本目の記事です。
はじめに
製造エネルギー事業部の辻です。Goのテストをはじめてみよう、という記事です。
この記事は、5年前の2020年に私が執筆したGoのテスト入門記事(Goのテストに入門してみよう!)のリメイク版です。当時は執筆したときのGoのバージョンは1.14でした。記事はありがたいことに継続的に反響をいただいていたものの、いくつか記述が古くなっていた点がありました。そこで今回Go1.15以降で導入された機能や、周辺のアップデート等を取り込み、改良しました。
Go のテストに関するヒント集としてお役に立てれば幸いです。
Tips
Go のテストの仕組みに、ベンチマークに関するテストと Example テストというサンプルコード用のテストも含まれているのですが、この2つは対象外にします。基礎的と思われる内容から順に並べてみました。
- Tips
- テストがしたい
- テストの失敗をレポートしたい
- テストをスキップしたい
- テストを並列に実施したい
- テストの前処理や後処理を実施したい
- カバレッジを取得したい
- あるディレクトリ配下のテストをすべて実施したい
- 一部のテストケースのみ実施したい
- サブテストの一部のみ実施したい
- テストのキャッシュを削除したい
- 構造体、マップやスライスの比較を実施したい
- APIサーバにアクセスするテストをしたい
- APIサーバのハンドラのテストをしたい
- データ競合をテストで検知したい
- テストデータを置いておきたい
- テストにヘルパー関数を使いたい
- ブラックボックステストをしたい
- 用途(単体・インテグレーションなど)によってテストを切り替えたい
- モックを使ってテストをしたい
テストがしたい
Goのテストは go test コマンドを用いてテストを実施します。テストを実施する関数の命名は以下のような形式でなければなりません。
func TestXxx(*testing.T) |
TestXxx は Testxxx ではダメです。Test_xxx という関数名であれば問題ありません。
テストファイルは xxxx_test.go といった命名である必要があります。このファイルはビルド時には除かれます。簡単なテストを試してみます。
package main |
https://play.golang.org/p/XYrpmljtPrW
go test |
上記のテストは TableDrivenTest とサブテストを組み合わせています。どちらもGoの開発現場でよく使われます。TableDrivenTestsについては公式のWikiも合わせて参照ください。またサブテストを用いると各テストごとに結果がわかるようになります。TableDrivenTest はさまざまな Input/Output パターンを網羅するのに便利です。参考までに上記のテストをTableDrivenTestを使わずに書き直すと以下のようになります。
func add(a, b int) int { |
https://play.golang.org/p/-TQObdTOqPA
テストの失敗をレポートしたい
Go はテストのアサーションを提供していません。理由は公式のFAQで紹介されています。先程の TestAdd() 関数のように、テストが失敗したことを開発者自ら実装する必要があります。失敗したことを示すには T.Error() や T.Fatal() を利用できます。
T.Fatal() を用いると T.Fatal() が実行された以降のテストは呼び出されずに終了します。テストが失敗したことを示すには T.Error() を使い、処理が失敗するとその後のテストが無意味になる場合は T.Fatal() を用いると良いでしょう。以下のように t.Fatal() を用いた場合は、その後のテストの処理 t.Log("after add() ...") が呼び出されていないことが分かります。ただし defer や T.Cleanup() といった後処理は呼び出されます。
func TestAdd(t *testing.T) { |
=== RUN TestAdd |
https://play.golang.org/p/2NZxz45zGD7
なお t.Fatalf と似たような関数名でログ出力してアプリケーションを終了する log パッケージの Fatalf という関数があります。log.Fatalf のGo Docにもあるように log.Fatalf は defer といった後処理を呼び出さず、即座に os.Exit(1) でアプリケーションが終了します。t.Fatalf と log.Fatalf を混乱しないように注意しましょう。
https://play.golang.org/p/ADm5xU7gsfm
テストをスキップしたい
時間を要するテストがあり、何らかの場合において、テストをスキップしたい場合があるかもしれません。次のような処理にめちゃくちゃ時間がかかる例を考えてみます。
func add(a, b int) int { |
このテストをスキップしたいとします。その場合は T.Skip() というメソッドを利用できます。
テストコードに以下を追加します。
b: 2, |
go test -v -short |
テストがスキップされていることが分かります。さらっと testing.Short() という関数も用いましたが Short() は testing パッケージに含まれている関数で、-short フラグがセットされていると true になります。そのためテストを実施するときに -short というフラグを付与したときだけテストがスキップされる、そうでないときはスキップされずテストが実施される、というように使い分けることができます。また -v オプションをつけてテスト実行することでサブテスト含め、実行したすべてのテスト結果を出力できます。
標準パッケージのテストにおいても、いくつかスキップしているケースを確認できます。以下は io/ioutil/ioutil_test.go からの抜粋です。特定の条件を満たす場合にテストをスキップするように実装されています。
func TestReadOnlyWriteFile(t *testing.T) { |
テストを並列に実施したい
以下のような謎に sleep する実装があったとしましょう。テストを効率的に実行するために並列化を試みます。
func add(a, b int) int { |
以下のサブテストを逐次実施すると 3 + 5 + 7 = 15 から約15秒テスト実施に時間がかかります。
func Test_add(t *testing.T) { |
go test -v |
並列にテストを実施するには T.Parallel() メソッドが利用できます。
for _, tt := range tests { |
テストが並列に実施されていることが分かります。結果としてテストにかかった時間は約7秒になりました。
go test -v |
Go 1.21以前はループ内で t.Parallel() で並列化するときなどにおいて、変数への参照が競合しないように以下の tt := tt といったメモリをループの内側で再確保する実装が必要でした。
Go1.22からはGo側でループごとに変数が作成されるようになったため、ユーザー側で tt := tt などとする必要はなくなりました。
for _, tt := range tests { |
なお、テストケース自体がそもそも並列に実行できない場合、例えばデータベース上のテーブルへの UPDATE や INSERT が発生し、テストケースで競合する場合、テストを並列に実行できないため、注意が必要です。
テストの前処理や後処理を実施したい
テスト実行にあたって、設定ファイルの読み込み・初期状態に戻すなどといった前処理や後処理をしたい場合があると思います。そのような共通的な前処理や後処理を実施したい場合に TestMain() 関数を利用できます。
例を見てみます。
func f() { |
TestMain() を用いて、テストの前後に処理を実行できます。
func Test_f(t *testing.T) { |
Test_f() の実行前後で前処理と後処理が実行されていることが分かります。
前処理 |
https://go.dev/play/p/2JbCk_jtPxw
Go 1.14 以前は TestMain() を利用する際に m.Run() の戻り値であるステータスコードを os.Exit() に渡す必要がありました。Go1.15 以降はユーザー側で os.Exit() を呼び出す必要はなくなりました。
func Test_f(t *testing.T) { |
また、後処理には T.Cleanup() が便利です。リソースの後処理という意味では defer を利用すればよいのでは? と思った方もいるかもしれません。大きな違いとして実行タイミングが挙げられます。
| 機能 | 実行タイミング |
|---|---|
defer |
呼び出し元の関数が return されたとき |
T.Cleanup() |
テストとそのすべてのサブテストが完了したとき |
T.Cleanup() を便利に使えることが実感できるシーンの1つとして、テストに必要な前処理をテストとは別の関数で実施している場合があります。
簡単な例ですが、テストの前準備としてテスト用のファイルを生成する必要があったとして、テスト終了後に削除したい場合、以下のような実装が考えられます。TempFile() 関数ではリソースをクローズするコールバック関数を呼び出し元に返却する必要があり、呼び出し元で後処理としてコールバック関数を呼び出します。
func TempFile(t *testing.T) (*os.File, func()) { |
呼び出し元の処理です。
file, cleanup := testutil.TempFile(t) |
T.Cleanup() を用いると前処理を実施する関数内でリソースの後処理が実施できるようになります。TempFile() 関数の例であれば、以下のように t.Cleanup() を利用できます。関数から return したタイミングで呼び出される defer とは異なり、テストが完了したタイミングで T.Cleanup() 処理が呼び出されます。
func TempFile(t *testing.T) *os.File { |
ファイルでなくても、テスト用のデータベースや HTTP サーバの teardown など、テストに必要なリソースの teardown 処理を同じ関数内に記述でき、テストコードを簡潔にできます。
カバレッジを取得したい
標準パッケージのカバレッジを取得してみます。今回は io パッケージ( ioutil も含む)のテストのカバレッジを取得することにします。カバレッジは go test コマンドの引数として -covermode=count を付与すれば取得できます。 -coverprofile=c.out で結果をファイルに保存します。
Go のコードは /Go/src にあるものとします。以下のコマンドを実行してカバレッジを取得します。
go test io/... -covermode=count -coverprofile=c.out |
以下のコマンドで上記の出力ファイルを簡単に HTML で可視化できます。
go tool cover -html=c.out -o coverage.html |
このような感じでどのパスが通っていないか確認できます。
環境変数を設定したい
単体テスト内のみで利用できる一時的な環境変数の設定に T.Setenv() を用いることができます。環境変数は os.Setenv() 関数でも設定できますが、os.Setenv() で設定した環境変数はプロセスの生存期間全体にわたって設定されます。あるテストのみで設定したい場合に T.Setenv() が有用です。ただし t.Parallel() を利用した並列テストでは利用できない点に注意が必要です。
以下はGo本体で利用されているテストコードです。
func TestBoringASN1Marshal(t *testing.T) { |
T.Setenv() はGo1.17で導入されました。Go1.16以前は以下のようなコードが慣習的でした。
func TestSomething(t *testing.T) { |
func TestSomething(t *testing.T) { |
あるディレクトリ配下のテストをすべて実施したい
たとえば標準パッケージの例だと、io パッケージはテスト対象に含めるが、その他のパッケージはテスト対象に含めない…といった要領です。これはテストのコマンドというよりはパッケージのコマンドになります。以下のように ... の文字列を用いてワイルドカードとしてテスト対象のファイルを選択できます。詳細は go help packages とすることで確認できます。
以下の場合は io パッケージに含まれるすべてのテストを実行します。
go test io/... |
同様に io パッケージに含まれる ioutil パッケージのみテストしたい場合は以下のようになります。
go test -v io/ioutil/... |
一部のテストケースのみ実施したい
実行対象のテストを抽出するには -run フラグを用いることできます。以下のように正規表現を用いて、一致するテストのみを実行できます。
-run regexp |
例えば io パッケージで Pipe に関するテストのみ実行したいとしましょう。Pipe に関するテストはテスト関数に Pipe の文字列が含まれるものとします。以下のように実行するとテストの関数名に Pipe が含まれるテストのみ実行されていることが分かります。
go test -v io/... -run Pipe |
サブテストの一部のみ実施したい
TableDrivenTest でテストを実装していると、ある1つのテストケースで複数のサブテストを実装します。サブテストの一部のみテストを実行したい場合はテストケースを絞り込むときと同様に -run フラグを用いることができます。-run=X/Y などとすると X にマッチするテストの Y にマッチするサブテストを実行できます。
func TestAdd(t *testing.T) { |
-run Add/mal とすると TestAdd() の normal のサブテストのみ実行されていることが分かります。サブテストがたくさんある場合で、一部のみ実行したい場合に便利です。
go test -v -run Add/mal |
テストのキャッシュを削除したい
Go でテストをしていると以下のように cached の文字列を見ることがあると思います。テストのキャッシュを削除するにはどのようにすれば良いでしょうか。
go test io/... -run Pipe |
テストのキャッシュは Go1.10 から組み込まれました。テストのキャッシュの詳細は go1.10#test が詳しいです。キャッシュ使わない場合はを明示的に -count=1 と指定すればよいです。-count=1 と明示的に指定するとテストはキャッシュされなくなります。
go test io/... -run Pipe -count=1 |
また go clean -cache を用いてもビルドキャッシュ全体を削除でき、テストのキャッシュも削除できます。
一時ディレクトリを作成したい
ファイルの読み書きに関するテストなどで、一時的なディレクトリやファイルを生成したいときがあります。Go1.15 から T.TempDir() というテスト時のみで利用できる一時ディレクトリを作成する機能が追加になりました。T.TempDir() ではテスト終了時にディレクトリとそのディレクトリ配下のファイルは削除されます。
T.TempDir() のサンプルコードを記載します。
func TestTempDir(t *testing.T) { |
=== RUN TestTempDir |
基本的には T.TempDir() で作成したディレクトリにファイルを作成してテストするのが良いでしょう。ただしテスト終了時に一時ディレクトリが削除されては困る、みたいな場合は T.TempDir() は利用できません。その場合は Goのテストでファイルの読み書きを扱いたい で詳しく解説していますので、こちらの記事を参照ください。
テストコードの雛形を楽に作りたい
小ネタですが IDE などの機能を使うと少し便利になるかもしれません。例えば GoLand の機能を用いるとデフォルトで TableDrivenTest の雛形を生成してくれます。詳細は割愛しますが VS Code を用いて開発する場合でも同様に雛形を生成できます。
構造体、マップやスライスの比較を実施したい
map のキーと値が一致しているか確認するようなテストがしたいとしましょう。map や slice は spec#Comparison_operators にもあるように比較演算子を用いて比較できません。map に含まれるキーと値が同じものを含んでいるかどうかはループを回して確認しないといけないのでしょうか。
reflect パッケージに含まれる reflect.DeepEqual を用いると map のような比較演算子で比較できないオブジェクトの比較ができます。
net/http パッケージにある request_test.go の中でクエリパラメータなどに用いられる map (map[string][]string 型)は以下のように比較して同値かどうかテストしています。
wantForm := url.Values{ |
Go は言語自体にテスティングフレームワークを提供してません。Where is my favorite helper function for testing? に理由が記載されていますが、端的に言うと開発者が適切にエラーハンドリングすることと適切なエラーメッセージの出力を実装することが重要であるためです。
とはいえ、薄い便利ライブラリを使いたくなるシチュエーションはあるでしょう。reflect.DeepEqual() を使った同値チェックは google/go-cmp を使うとより便利にテストができます。私は reflect.DeepEqual() の代わりとして google/go-cmp を用いることが多いです。
go-cmp のリポジトリにある構造体を比較するサンプルの例を見てみます。以下のような net.IP 型や time.Time 型をフィールドに持つ構造体の比較です。
type ( |
この構造体が同値かテストしてみます。上記の実装の通り MakeGatewayInfo は異なる 2 つの変数 x と y を返す関数です。これを go-cmp と reflect.DeepEqual() のそれぞれを用いて比較してみます。
func TestMakeGatewayInfoDeepEqual(t *testing.T) { |
実行すると以下のような結果を得ることができます。
go test example_test.go |
go-cmp の利点の1つとして、テストの出力から、何が同値で、何が同値でなかったか、同値でなかったときは取得した値と想定する値は何であったか明示的にわかる点が挙げられます。他にもオプションで条件をカスタマイズできます。
便利ライブラリに関しては社内でも stretchr/testify/assert などのライブラリを使うGopherを見かけます。Go のテスティングライブラリに関しては好みが分かれるところだと思うので、深くは触れません。
APIサーバにアクセスするテストをしたい
net/http/httptest を利用すると簡単にテスト用のモックサーバを用意できます。リクエストに対して Hello, client というレスポンスを返却する API を用いた、API クライアントの視点で単体テストを実施する例を考えてみます。
httptest.NewServer にハンドラを渡すことでハンドラの振る舞いをするローカルの HTTP サーバを提供してくれます。デフォルトだとこのテスト用の HTTP サーバは 127.0.0.1:0 で起動します。ポートが 0 にバインドされていますが、これは空いている任意ポートを割り当てます。なお、ブラウザからデバッグしたいなどの用途として httptest.serve フラグを渡すこともできます。-httptest.serve=127.0.0.1:18888 などとしてフラグを指定した場合、HTTP サーバはブロックされます。
func TestX(t *testing.T) { |
https://play.golang.org/p/i1Dz5alS4hQ
実際にはテストで用いる構造体のフィールドに URL を保持できるようにして httptest で起動させたモックサーバの URL に切り替えるなどことが多いのではないか、と思います。
func setup() (client *Client, mux *http.ServeMux, serverURL string, teardown func()) { |
APIサーバのハンドラのテストをしたい
上記の「APIサーバにアクセスするテストをしたい」の項目では httptest.NewServer でモックサーバを立てて、API クライアントという視点でリクエストを発行してテストをしました。今度は API サーバを提供する視点から、ハンドラの単体テストを実施する場合を考えてみます。httptest.NewRequest を用いると、ハンドラの単体テストを簡潔に実施できます。以下のようなハンドラを考えてみます。
func helloHandler(w http.ResponseWriter, r *http.Request) { |
このハンドラに対して GET リクエストしたときに hello world! というレスポンスが得られるかどうかテストします。httptest.NewRequest はテストのためのリクエストを生成するのに便利です。httptest.NewRequest で生成した *http.Request をハンドラに渡すことができます。また httptest.NewRecorder() で生成できる *httptest.ResponseRecorder を用いるとハンドラのレスポンスを記録できます。
これらを用いて、以下のようにハンドラから想定のレスポンスが得られるかどうかテストできます。
import ( |
go test -v |
httptest.NewRequest() や httptest.NewRecorder() を用いることで、Echo や go-swagger といった Web フレームワークを用いて構築しているハンドラも同様にテストできます。
ちなみにリクエストは http.NewRequestWithContext (http.NewRequest) でも生成できるのは? と思うかもしれません。何が異なるかというと、httptest.NewRequest は http.Handler に適しているリクエストを返却します。一方で http.NewRequestWithContext (http.NewRequest) は Client.Do や Transport.RoundTrip に適しているリクエストを返却します。そのため、単体テストの用途で直接ハンドラにリクエストを渡す場合は httptest.NewRequest を用いることになります。
データ競合をテストで検知したい
テストでレースコンディションを検知したい場合はどうすればよいでしょうか。レースコンディションが起きていないか確認するために -race フラグが役に立ちます。
公式の The Go Blog の例を引用します。
package main |
上記の実装は異なるゴルーチンから変数 t の読み書きをしていて、レースコンディションを引き起こします。これは -race をつけてテストすることで確認できます。
go test -v -race race_test.go |
上記のようにレースコンディションが発生している場合は、テストが FAIL になります。
-race をつけてテストすると多少実行に時間がかかりますが、想定しないレースコンディションが発生して、テストが FAIL になるとデバッグが大変です。テストの際にデフォルトで -race をつけておくのも1つの方法です。
テストデータを置いておきたい
テストの Input や Output になるファイルを testdata という名前のディレクトリに置いておくことができます。testdata ディレクトリに含まれるファイルはテストのときのみ用いられます。標準パッケージの中でたくさん用いられていますが、image パッケージの例を上げると src/image/testdata といったものです。
テストにヘルパー関数を使いたい
複数のテストで用いるような共通の関数をヘルパー関数として実装する場合があると思います。テスト用のヘルパー関数の特徴は以下です。
- ヘルパー関数はエラーを返さない
*testing.Tを受け取ってテストを落とす- Go 1.9 からは
T.Helperを使って情報を補足する
Go Friday 傑作選 より引用
テスト用のヘルパー関数は以下のように実装されます。
func mustUrlParse(t *testing.T, s string) *url.URL { |
ヘルパー関数に t.Helper() を付与すると何が嬉しいのでしょうか。上記のヘルパー関数 mustUrlParse を用いたテストを実施してみて確認してみます。
func TestX(t *testing.T) { |
- ヘルパー関数から
t.Helper()を除いた場合
go test -v |
- ヘルパー関数に
t.Helper()がある場合
go test -v |
上記の 2 通りの結果を比較すると FAIL になった行が何行目を指しているのか、が変わっています。
// t.Helper() がない場合 |
t.Helper() を付けた場合は t.Run() 内の _ = mustUrlParse(t, tt.args.str) の行を指しています。付けなかった場合はヘルパー関数でエラーになった t.Fatal(err) の行を指しています。t.Helper() を付けた場合のほうが、テストケース内のどの行で失敗したか分かりやすくなり、エラーの原因を探りやすくなります。
ブラックボックステストをしたい
あるパッケージ mypkg があって以下のようなファイル構成になっているとします。ブラックボックステストなどを実施するときにパッケージ名を分けたい、などのニーズがあるかもしれません。Go ではディレクトリ名とパッケージ名は同じである必要があるため、異なるパッケージ名のファイルを作ることはできません。しかし例外的に mypkg_test という、mypkg に _test を付与したパッケージ名は用いることができます。Test packages にもあるように Go では _test の接尾語をもつパッケージ名は別のパッケージとして扱われます。パッケージ名を mypkg と mypkg_test とで分けることでテスト実施時に mypkg の非公開の関数を呼び出すことはできなくなります。似たような議論を Proper package naming for testing with the Go language で見ることができます。
具体的には以下のようなファイル構成になっていて x.go と x_test.go で別のパッケージ名にできます。ディレクトリは分けなくて良い、というのがポイントです。
mypkg |
package mypkg |
// mypkg ディレクトリにあるテストファイルでも _test を付けたパッケージ名を混在できる |
用途(単体・インテグレーション)などによってテストを切り替えたい
テストは単体テストの他、インテグレーションテストなど用途によっていくつか存在します。用途によって実行するテストを分けるにはどのようにすれば良いでしょうか。ビルドタグ を用いる方法やディレクトリを分ける方法があります。
- ビルドタグを用いる方法
以下のようにビルドタグ // +build integration を付与します。このようにすると go test -tags=integration とタグを付けたときのみテストが実行されるようになります。タグがない場合はテスト対象に含まれません。
// +build integration |
OSS から具体例をあげると以下のようなものです。以下の例はテストのパッケージ名に _test をつけることでテストであることを明示的に分けています。テストで用いる関数は、テスト内でインポートしています。
// Copyright 2019 Google Inc. All Rights Reserved. |
https://github.com/google/mtail/blob/master/internal/mtail/new_prog_integration_test.go#L1-L44
- ディレクトリを分ける
シンプルながら、インテグレーションテストは integration などのディレクトリに対象のテストケースを含めることが多いのではないでしょうか。その integration パッケージと // +build integration のビルドタグを組み合わせる方法も考えられます。OSS だと以下のようなイメージです。
google/go-github
モックを使ってテストをしたい
外部サービスに依存する実装のテストをするときに、外部サービスをモックしたい場合があります。特に外部の API を実行した結果に依存する場合、API のレートリミットやテストの結果が外部 API の仕様変更などに引きづられ、テスト結果が不安定になります。どのようにすればモック化できるでしょうか。方法はいくつかありますが、基本的な考えはインターフェースを満たす実装を、本物の実装とモックで切り替えていくことになります。
データベースアクセスする処理をモックする簡単な疑似コードを見てみます。データベースからユーザの検索と作成をします。インターフェースのメソッドは以下のようにあるとします。
type DataStore interface { |
type User struct { |
通常の実装は以下のような感じを想定します。ポイントは DataStore のインターフェースをファクトリ関数である NewDataStore で返すところです。
type UserDataStore struct { |
あるテストをする際に、このデータベースに依存する実装をモックする例は以下のようになります。インターフェースを満たす、モックオブジェクトに想定の結果が取得できるメソッドを実施します。
type MockUserDataStore struct{} |
このモックしたオブジェクトをテストで用いる場合は以下のようになります。
func TestXXX(t *testing.T) { |
上記のようにして外部依存する処理をモックできます。モックの struct やメソッドの実装が手間なので gomock を使うこともあるかもしれません(gomock に関して詳細は説明を割愛します)。
インターフェースを用いたテストに関しては Golangにおけるinterfaceをつかったテスト技法 が詳しいです。またインターフェースの埋め込みを用いたモックに関しては Golangにおけるinterfaceをつかったテストで Mock を書く技法 が詳しいです。
余談ですが、テストがモックに依存しすぎるとなんのためのテストをしているか分からなくなったり、リファクタリングのときに辛かったりするので、モックを使ったテストは用法用量に注意です。
コンテナを扱いたい
モックの例は上記に記載したものの、コンテナなどを利用して実際のサービスに近い環境でテストしたい場合があるでしょう。コンテナを利用する際に docker compose up などで単体テストに必要なサービスを起動させてテストする、という方法がありますが、この記事ではテストコードの中でコンテナを扱える Testcontainers を簡単に紹介します。
Testcontainers とはテストコード上で任意のコンテナを起動・停止できるドライバのようなライブラリです。Go に限らずJava, Python, Rustなど様々な言語をサポートしています。Testcontainers の利点の1つとして、外部プロセスに依存せず、テストコードで必要な依存関係を制御できることが挙げられます。
以下はコンテナとして Redis を利用したサンプルコードです。コードの詳細には立ち入りませんが、テストコードと実行結果からコンテナの起動と停止、削除している雰囲気がつかめると思います。
import ( |
実行ログから以下のようにコンテナの作成、起動、停止、削除がされていることが分かります。
go test -v |
詳しくは Testcontainersを用いてテスト実行前の docker compose up を無くし、Goで並列テストする の記事で解説しているので、合わせて参照ください。
ゴルーチンリークを検出したい
ゴルーチンがリークするような実装になっていることをテスト時に確認したいかもしれません。公式のツールチェーンには含まれていませんが、goleak はゴルーチンリークの検出に役に立ちます。このツールを用いるとテスト時にゴルーチンがリークしている場合にテストが FAIL になります。
簡単な例で確認してみます。
func X(send chan struct{}) { |
func TestDoLeak(t *testing.T) { |
結果は以下のようになります。想定通り leak のテストが FAIL になっていることが分かります。
go test -v |
https://play.golang.org/p/wWGWe8_S0WN
まとめ
テストに関する Go の基本的な機能をまとめてみました。なるべく使い方のイメージがわかるように具体的な例を多く載せました。参考にした素晴らしいドキュメントの URL は以下に記載しています。合わせてみてみてください。
参考
- https://blog.golang.org/race-detector
- https://github.com/golang/go/wiki/LearnTesting
- https://golang.org/cmd/cover/
- https://golang.org/cmd/go/
- https://golang.org/doc/articles/race_detector.html
- https://golang.org/pkg/cmd/go/internal/test/
- https://golang.org/pkg/testing
- https://talks.godoc.org/github.com/tcnksm/talks/2016/12/golang-tokyo/golang-tokyo.slide
- https://talks.golang.org/2014/testing.slide