Go Wiki: Go Test Comments

このページはGo Code Review Commentsの補足ですが、テストコードに特化しています。

このページを編集する前に、たとえ些細な変更であっても、変更について議論してください。多くの人が意見を持っており、ここは編集合戦の場ではありません。

アサートライブラリ

テストを助けるために「アサート」ライブラリを使用することは避けてください。xUnitフレームワークから来たGo開発者は、次のようなコードを書きたがることがよくあります。

assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
assert.StringNotEq(t, "obj.Body", obj.Body, "")

しかし、これはテストを早期に停止させるか(アサートがt.Fatalfまたはpanicを呼び出す場合)、テストが正しかった興味深い情報を省略します。また、アサートパッケージは、既存のプログラミング言語(Go自体)を再利用する代わりに、全く新しいサブ言語を作成することを強制します。Goには構造体の出力に関する優れたサポートがあるため、このコードを書くより良い方法は次のとおりです。

if obj == nil || obj.Type != "blogPost" || obj.Comments != 2 || obj.Body == "" {
    t.Errorf("AddPost() = %+v", obj)
}

アサートライブラリは、不正確なテストを簡単に書かせ、最終的には式評価、比較、時にはそれ以上のものなど、言語にすでに存在する機能を重複させることになります。何がうまくいかなかったのか、何がうまくいったのかについて正確なテストを書くように努め、Goの内部にミニ言語を作成する代わりにGo自体を使用してください。

人間が読めるサブテスト名を選ぶ

t.Runを使用してサブテストを作成する場合、最初の引数はテストの説明的な名前として使用されます。テスト結果がログを読む人間にとって判読可能であることを保証するために、エスケープ後も有用で読みやすいサブテスト名を選択してください。(テストランナーはスペースをアンダースコアに置き換え、非表示文字をエスケープします)。

入力を特定するには、サブテスト本体でt.Logを使用するか、テストの失敗メッセージに含めます。そこではテストランナーによってエスケープされません。

完全な構造体を比較する

関数が構造体を返す場合、構造体の各フィールドに対して個別の比較を実行するテストコードは書かないでください。代わりに、関数が返すことを期待する構造体を構築し、差分または深い比較を使用して一度に比較します。同じルールは配列とマップにも適用されます。

構造体が近似的な等価性やその他の種類の意味的等価性について比較する必要がある場合、または等価性について比較できないフィールドが含まれている場合(例:フィールドの1つがio.Readerである場合)、cmp.Diffまたはcmp.Equal比較をcmpoptsオプション(例:cmpopts.IgnoreInterfaces)で調整することでニーズを満たすことができます()。そうでない場合、この手法は機能しないため、機能するものを使用してください。

関数が複数の戻り値を返す場合、それらを比較する前に構造体にラップする必要はありません。個別に比較して出力するだけです。

安定した結果を比較する

制御できない外部パッケージの出力安定性に本質的に依存する結果の比較は避けてください。代わりに、テストは、安定しており、依存関係の変更に影響されない意味的に関連する情報に基づいて比較する必要があります。フォーマットされた文字列またはシリアライズされたバイトを返す機能の場合、出力が安定していると仮定するのは一般的に安全ではありません。

たとえば、json.Marshalは、出力する正確なバイトについて保証しません。出力は変更される自由があり(過去にも変更されています)、jsonパッケージがバイトのシリアル化方法を変更した場合、正確なJSON文字列に対して文字列等価性を行うテストは壊れる可能性があります。代わりに、より堅牢なテストは、JSON文字列の内容を解析し、それがいくつかの期待されるデータ構造と意味的に同等であることを確認します。

等価比較と差分

==演算子は、言語定義の比較を使用して等価性を評価します。比較できる値には、数値、文字列、ポインタ値、およびそれらの値のフィールドを持つ構造体が含まれます。特に、2つのポインタは、同じ変数を指している場合にのみ等しいと判断されます。

cmpパッケージを使用してください。等価比較にはcmp.Equalを、オブジェクト間の人間が読める差分を取得するにはcmp.Diffを使用してください。

cmpパッケージはGo標準ライブラリの一部ではありませんが、Goチームによって保守されており、Goバージョンの更新全体で安定した結果を生成するはずです。ユーザーが設定可能であり、ほとんどの比較ニーズに対応するはずです。

古いコードでは、複雑な構造を比較するために標準のreflect.DeepEqual関数を使用していることを見つけるでしょう。新しいコードにはcmpを好み、可能であれば古いコードをcmpを使用するように更新することを検討してください。reflect.DeepEqualは、非公開フィールドやその他の実装詳細の変更に影響を受けやすいです。

注:cmpパッケージは、プロトコルバッファメッセージを比較する際にcmp.Comparer(proto.Equal)オプションを含めることで、プロトコルバッファメッセージでも使用できます。

Got before Want

テスト出力は、期待される値を出力する前に、関数が実際に返した値を出力するべきです。テスト出力の通常の形式は、「YourFunc(%v) = %v, want %v」です。

差分の場合、方向性はあまり明確ではないため、失敗を解釈するのに役立つキーを含めることが重要です。差分を印刷するを参照してください。

失敗メッセージで使用する順序に関係なく、既存のコードは順序付けに関して一貫性がないため、失敗メッセージの一部として順序を明示的に示す必要があります。

関数を特定する

ほとんどのテストでは、テスト関数の名前から明らかであるように思えても、失敗メッセージには失敗した関数の名前を含めるべきです。

以下を推奨します

t.Errorf("YourFunc(%v) = %v, want %v", in, got, want)

以下は推奨しません

t.Errorf("got %v, want %v", got, want)

入力を特定する

ほとんどのテストでは、入力が短い場合、テスト失敗メッセージには関数の入力を含めるべきです。入力の関連するプロパティが明らかでない場合(たとえば、入力が大きいまたは不透明なため)、テストケースにテスト内容の説明を付け、エラーメッセージの一部として説明を出力するべきです。

テストに名前を付けたり入力を出力したりする代わりに、テストテーブル内のテストのインデックスを使用しないでください。テストテーブルを見てエントリを数え、どのテストケースが失敗しているかを特定したい人はいません。

継続する

テストケースが失敗に遭遇した後でも、可能な限り長く実行を続け、単一の実行で失敗したすべてのチェックを出力するべきです。このようにすることで、失敗したテストを修正する人は、モグラたたきをする必要がなく、1つのバグを修正してからテストを再実行して次のバグを見つける必要がなくなります。

実際には、t.Fatalよりもt.Errorを呼び出すことを好みます。関数の出力のいくつかの異なるプロパティを比較する場合、それぞれの比較にt.Errorを使用します。

t.Fatalは、テストセットアップの一部が失敗し、それなしではテストをまったく実行できない場合にのみ適切です。テーブル駆動テストでは、テストループの前にテスト関数全体を設定する失敗に対してt.Fatalが適切です。テストテーブルの単一のエントリに影響し、そのエントリで続行できない失敗は、次のように報告する必要があります。

  • t.Runサブテストを使用しない場合、次のテーブルエントリに進むためにt.Errorの後にcontinue文を使用する必要があります。
  • サブテストを使用している場合(そしてt.Runの呼び出しの中にいる場合)、t.Fatalは現在のサブテストを終了し、テストケースが次のサブテストに進むことを許可するため、t.Fatalを使用してください。

テストヘルパーをマークする

テストヘルパーは、入力メッセージの作成など、テスト対象のコードに依存しないセットアップまたはティアダウンタスクを実行する関数です。

*testing.Tを渡す場合、t.Helperを呼び出して、テストヘルパー内の失敗をヘルパーが呼び出された行に帰属させます。

func TestSomeFunction(t *testing.T) {
  golden := readFile(t, "testdata/golden.txt")
  // ...
}

func readFile(t *testing.T, filename string) string {
  t.Helper()

  contents, err := ioutil.ReadFile(filename)
  if err != nil {
    t.Fatal(err)
  }

  return string(contents)
}

テストの失敗とその原因となった条件とのつながりを不明瞭にする場合は、このパターンを使用しないでください。具体的には、t.Helperはアサートライブラリを実装するために使用すべきではありません。

関数が大量の出力を返す場合、テストが失敗したときに、失敗メッセージを読んでいる人が違いを見つけるのが難しいことがあります。返された値と期待された値の両方を出力する代わりに、差分を作成してください。

差分の方向を説明するテキストを失敗メッセージに追加してください。

cmpパッケージを使用している場合(関数に(want, got)を渡す場合)、「diff -want +got」のようなものは良いです。なぜなら、フォーマット文字列に追加する-+が、差分行の先頭に実際に表示される+-と一致するからです。

差分は複数行にわたるため、差分を出力する前に改行を出力する必要があります。

テーブル駆動テスト vs 複数のテスト関数

テーブル駆動テストは、多数の異なるテストケースを類似のテストロジックを使用してテストできる場合に常に使用する必要があります。たとえば、関数の実際の出力が期待される出力と等しいかどうかをテストする場合()、または関数の出力が常に同じ一連の不変条件に準拠しているかどうかをテストする場合などです。

一部のテストケースを他のテストケースとは異なるロジックでチェックする必要がある場合は、複数のテスト関数を記述する方が適切です。テーブル内の各エントリが、適切な種類の入力に対して適切な種類の出力チェックを行うために、複数種類の条件ロジックにかけられなければならない場合、テストコードのロジックは理解しにくくなる可能性があります。異なるロジックでも設定が同じであれば、単一のテスト関数内のサブテストのシーケンスも理にかなっているかもしれません。

テーブル駆動テストと複数のテスト関数を組み合わせることができます。たとえば、関数のエラー以外の出力が期待される出力と完全に一致するかどうかをテストし、かつ、無効な入力が与えられたときにその関数が非nilエラーを返すこともテストする場合、最も明確なユニットテストは、2つの別個のテーブル駆動テスト関数(通常の非エラー出力用とエラー出力用)を記述することで実現できます。

テストエラーのセマンティクス

ユニットテストが文字列比較を実行したり、reflect.DeepEqualを使用して特定の種類の入力に対して特定の種類のエラーが返されることをチェックしたりする場合、将来的にそれらのエラーメッセージのいずれかを修正する必要がある場合、テストが壊れやすいことに気づくかもしれません。これはユニットテストを変更検出器に変える可能性があるため、関数の返すエラーの種類をチェックするために文字列比較を使用しないでください。

テスト対象のパッケージから来るエラーメッセージが、たとえばパラメータ名を含むなど、あるプロパティを満たしていることをチェックするために文字列比較を使用することは問題ありません。

関数の返すエラーの正確なタイプをテストすることに関心がある場合、人間が見るためのエラー文字列と、プログラムで使用するために公開される構造を分離する必要があります。この場合、意味のあるエラー情報を破壊する傾向があるfmt.Errorfの使用は避けるべきです。

APIを作成する多くの人々は、APIが異なる入力に対してどのような種類のエラーを返すかを正確に気にしません。APIがこのような場合、fmt.Errorfを使用してエラーメッセージを作成し、ユニットテストで、エラーが期待されたときにエラーが非nilであったかどうかのみをテストするだけで十分です。


このコンテンツはGo Wikiの一部です。