はじめに
TIG真野です。
フューチャー技術ブログ Go 1.24 リリース連載の2本目です(公開は1ヶ月程度遅れたので、時系列では7本目です)。
Go 1.24で追加された testing.Context
(T.Context
) について紹介します。
func (c *T) Context() context.Context` |
- Cleanup に登録された関数が呼び出される直前にキャンセルされるコンテキストを返す
- Cleanup でテストまたはベンチマークが完了する前に、Context.Done によってシャットダウンされるリソースを待機できる
testing.Context
は定期的に欲しいな⇛プロポーサルが却下されているな..を繰り返してきたので、Go 1.24で入ってとても嬉しく思っています。2年半くらい前にもポストしていました。
testing.Context() のヘルパー関数欲しい(context.Background() 書くのがやや冗長に感じる)
— Junki Mano (@ma91n) September 20, 2022
注目度が高いこともあって、 testing.Context
について詳細に説明した記事がいくつも公開されています。
- Goのtesting.TにContext()メソッドが追加されそうという話 - 焼売飯店
- Go 1.24の新機能testing.T.Context() がやってきたから徹底解説する!
- Go1.24で導入されたt.Contextにシュッと対応する
そのため、testing: reconsider adding Context method to testing.T #36532 のIssueの議論のやり取りや、仕様について私なりに気になった点を深堀りすることを中心にします。
Discussion #36532サマリ
T.Context()
を追加する提案は観測できる範囲で2016年ごろから断続的に行われていたT.Context()
から取得したテスト終了後にキャンセルするコンテキストを利用することで、例えば、テストコード上でgoroutine呼び出ししていた場合も、テストの停止とタイミングを併せて停止させることができると期待された#16221
では実装まで行ったが、コンテキストをキャンセルして実際にgoroutineが全て停止したことを確認待ちする手段が無かった。そのため、テストコード上で起動したgoroutineが残ったまま次のテストが動く可能性があり、課題が解決しておらず不完全であるためリバートされた(※後述)- 2020年3月リリースのGo 1.14 で
t.Cleanup()
が実装され、これと組み合わせると待ち合わせが可能となり、課題が消えたためT.Context()
が追加可能となった。プロポーサル自体は2020年に起票されていて、2024年6月ごろに議論が再び活発になった流れでした T.Context()
が存在しない場合でも、暫定的な回避方法は存在していたが(※後述)、それでもT.Context()
が追加したほうが自然であり、また副作用も少ないと見てゴーの判断となった
Try it
Issueの説明に記載されたコードを少し改変し動かします。
package main |
これを実行します。
go test -run TestTContext_Wait |
結果は time.Sleep(500 * time.Millisecond)
で指定した 500ms だけworker() のループが5回実行され、その後 TestTContext_Wait()
が終了。コンテキストがキャンセルされ、Worker1: context canceled: context canceled
というメッセージを出してworkerが終了。その後、 t.Cleanup()
が呼ばれるという流れです。
図にすると以下にようなイメージです。

コンソールログや上図を見ると、 t.Cleanup()
の wg.Wait()
不要じゃね?と思うでしょう(私は思いました)。そのため、別のコードを書いてみます。
package main |
これを実行すると以下のようになります。
go test -run TestTContext_Wait |
興味深いのは、 Worker1: context canceled: context canceled
が、次のテストである TestTContext_Wait_Next
が起動された後に呼び出されていることです。つまり、TestTContext_Wait
で呼び出されたgoroutineが、次のテスト中に存命であるという状態が起こりえるということです。
サマリのタイミングでさらっと説明した通り、 T.Cleanup()
が存在しないタイミングでは、 T.Context()
が存在しても中途半端だというのは、こういったことを指していました。
一応イメージを図にします。t.Cleanup()
で終了を待ち合わせる重要性が分かりました。

気になった点
私がリリースノートのT.Context()
部分を読んだところで、気になった点を残していきます。
t.Fail()
された場合にはキャンセルされるのか?
キャンセルされませんでした(ドキュメント上も、T.Cleanup()前にキャンセルされるとあるので、それはそう)。
func TestTContext_Fail(t *testing.T) { |
実行結果。
go test -run TestTContext_Fail |
テストがタイムアウトした場合に呼ばれるのか?
キャンセルされませんでした。
func TestTContext_Timeout(t *testing.T) { |
-timeout
オプション付きで実行します。
go test -run TestTContext_Timeout -timeout 1s |
従来通り panic で終了する感じです(それはそう)。考えてみればテスト全体の実行時間のタイムアウトなので、プロセスごと落とすで良く、何かハンドリングしたいケースは少なそうです(DBコネクションを明示的にクローズしたいとかはあるかもですが)。
TestMain()
で呼べないのか?
func TestMain(m *testing.M) {...}
では呼べないです。 testing.M
には追加されていないです。
ただし、testing.B
には追加されています。
サブテストで T.ContexT()
を呼んだらどうなるのか?
サブテストのスコープでキャンセルされましt
package main |
実行します。サブテストごとに設定したt.Cleanup() の呼ばれる前(ログ上はworkerにキャンセルが伝播されて後ログ出力するまでの遅延があり、入れ帰っていますが)に、キャンセルされていることがわ
go test -run TestSubtests |
従来の回避方法は?
T.Context()
を用いずとも、context.WithCancel()
で同等のことが実現できました。Issueのやりとりでは、ctx, cancel := context.WithCancel(context.Background()); defer cancel()
で代用できるし、これらは2行とシンプルだし、新しくAPIを追加する必要はないのではないか?といったコメントもありました。
context.WithCancel()
で書いたコードです。
func TestTContext_WorkAround(t *testing.T) { |
実行します。
go test -run TestTContext_WorkAround |
これを見ると、t.Cleanup()
でコンテキストをキャンセルし、かつ wg.Wait()
でテストコード中に起動したgoroutineが全て終了していることを確認して、テストが終了しています。 T.Context()
が無くても大丈夫です。では、なぜ追加が決まったのでしょうか。
これについては Ianさんのコメント が流れを決めた気がしています。主張としては以下です。
背景:
- 複数の企業でテストを追加した経験上、テスト数が増えるとリソースリークに対する課題が出る(既存のテストと似た別のテストを追加した後、テストを全実行すると動かなくなることが1回は出てくる)
- 各テストの先頭に defer を含むキャンセル可能なコンテキストに書き換えすることで、回避はできた
- ただ、その後にまたキャンセル可能なコンテキストを使っていないテストが紛れ込むことがあり、都度修正した
主張:
- (
T.Context()
やcontext.WithCancel()
を問わず)テストの終了時にコンテキストをキャンセルしないと、リークの問題が発生し、デバッグが困難 - 上記のような経験が無いと、通常
ctx, cancel := context.WithCancel(context.Background()); defer cancel()
を利用することはないし、あったとしても一貫して使用するには面倒
さらに、「context.Background()
を T.Context()
に書き換えれば、テストの最後にキャンセルされたコンテキストの恩恵を受けるか、最悪の場合でも影響を受けない」とあり、確かに副作用はほぼないと感じました。
context.WithCancel()
とdefer
や t.Cleanup()
の組み合わせで代替できるが、多くの開発者が context.Background()
をテストで使ってしまうことで、不具合が出てしまっている。これを T.Context()
でそのまま置き換えて使ってしまっても、メリットはあれど副作用は考えにくい、ということで、納得感がありました
さいごに
T.Cleanup()
が T.Context()
追加するために重要なピースだったということはIssueを読めばすぐ分かりましたが、具体的にどういう意味なのか、コードを動かしてみるまでピンと来ませんでした。手を動かして疑問点を潰しこむ重要性を、Goリリースノート連載はいつも私に教えてくれます。
超個人的には、sync.WaitGroup
や errgroup
を使用する機会は少ないためテストでハマることは少ないため、context.Background()
って書くより t.Context()
で書け、文字数が減るというのが一番の嬉しいポイントでした。
最近、業務でGoを書く機会が減っていますが、引き続きGo情報はウォッチして楽しみながら精進したいと思います。