Go Wiki: Goテストのコメント
このページはGoコードレビューのコメントの補足ですが、特にテストコードを対象としています。
**このページを編集する前に、たとえ*小さな*変更であっても、変更について議論してください。** 多くの人が意見を持っており、ここは編集合戦をする場所ではありません。
- アサートライブラリ
- 人間が読めるサブテスト名を選択する
- 安定した結果を比較する
- 構造体全体を比較する
- 等価比較と差分
- 期待値の前に取得値
- 関数を特定する
- 入力を特定する
- 継続する
- テストヘルパーをマークする
- 差分を出力する
- テーブル駆動テスト vs 複数のテスト関数
- エラーのセマンティクスをテストする
アサートライブラリ
テストを支援するために「アサート」ライブラリの使用は避けてください。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, "")
しかし、これはテストを早期に停止するか(assertがt.Fatalf
またはpanic
を呼び出す場合)、またはテストが正しく取得した内容に関する興味深い情報を省略します。また、assertパッケージは既存のプログラミング言語(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.Comparer(proto.Equal)
オプションを含めることにより、cmp
パッケージをプロトコルバッファメッセージで使用することもできます。
期待値の前に取得値
テスト出力は、期待値を出力する前に、関数が返した実際の値を出力する必要があります。テスト出力の通常の形式は「YourFunc(%v) = %v, want %v
」です。
差分の場合、方向性がわかりにくいため、失敗の解釈に役立つキーを含めることが重要です。差分を出力するを参照してください。
失敗メッセージでどちらの順序を使用する場合でも、既存のコードは順序に関して一貫性がないため、順序付けを失敗メッセージの一部として明示的に示す必要があります。
関数を特定する
ほとんどのテストでは、テスト関数名から明らかであるように見えても、失敗メッセージには失敗した関数の名前を含める必要があります。
好ましい例:
t.Errorf("YourFunc(%v) = %v, want %v", in, got, want)
```go
t.Errorf("got %v, want %v", got, want)
入力を特定する
好ましくない例:
```go
継続する
ほとんどのテストでは、テストの失敗メッセージには、関数の入力が短い場合はそれらを含める必要があります。入力の関連プロパティが明らかでない場合(たとえば、入力が大きすぎるか不透明なため)、テスト対象の内容を説明するテストケースに名前を付け、説明をエラーメッセージの一部として出力する必要があります。
テストテーブルのテストのインデックスを、テストに名前を付けたり入力を出力したりするための代替として使用しないでください。どのテストケースが失敗しているかを把握するために、テストテーブルを調べてエントリを数えたいと思う人はいません。
テストケースで障害が発生した後でも、1回の実行ですべての失敗したチェックを出力するために、可能な限り長く続行する必要があります。こうすることで、失敗したテストを修正している人は、1つのバグを修正してからテストを再実行して次のバグを見つけるという、もぐら叩きをする必要がなくなります。
- 実際には、
t.Fatal
よりもt.Error
の呼び出しを優先してください。関数の出力のいくつかの異なるプロパティを比較する場合、それらの比較ごとにt.Error
を使用します。 t.Fatal
は通常、テストをまったく実行できないテスト設定の一部が失敗した場合にのみ適しています。テーブル駆動テストでは、t.Fatal
はテストループの前にテスト関数全体を設定する障害に適しています。テストテーブルの単一のエントリに影響を与える障害で、そのエントリを続行できない場合は、次のように報告する必要があります
テストヘルパーをマークする
`t.Run` サブテストを使用していない場合は、`t.Error` を呼び出してから `continue` 文を使用して、次のテーブルエントリに移動する必要があります。
サブテストを使用している場合(および `t.Run` の呼び出し内にある場合)、`t.Fatal` は現在のサブテストを終了し、テストケースが次のサブテストに進むことができるようにするため、`t.Fatal` を使用します。
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)
}
テストヘルパーは、テスト対象のコードに依存しない入力メッセージの構築などの設定またはティアダウンタスクを実行する関数です。
差分を出力する
*testing.T
を渡す場合は、t.Helper
を呼び出して、テストヘルパーの失敗をヘルパーが呼び出された行に帰属させます。
テストの失敗とその原因となった条件との関係がわかりにくくなる場合は、このパターンを使用しないでください。具体的には、t.Helper
を使用してアサートライブラリを実装しないでください。
関数が大きな出力を返す場合、テストが失敗したときに失敗メッセージを読んでいる人が違いを見つけるのが難しい場合があります。返された値と必要な値の両方を出力する代わりに、差分を作成します。
差分の方向を説明するテキストを失敗メッセージに追加します。
テーブル駆動テスト vs 複数のテスト関数
cmp
パッケージを使用している場合(関数を(want、got)
に渡す場合)、「diff -want +got
」のようなものは適しています。これは、フォーマット文字列に追加した-
と+
が、差分行の先頭に実際に表示される+
と-
と一致するためです。
いくつかのテストケースを他のテストケースとは異なるロジックでチェックする必要がある場合は、複数のテスト関数を作成する方が適切です。 テーブルのすべてのエントリに対して、適切な種類の入力に対して適切な種類の出力チェックを実行するために、複数の種類の条件付きロジックを適用する必要がある場合、テストコードのロジックは理解しにくくなる可能性があります。 ロジックは異なるがセットアップが同じ場合は、1つのテスト関数内の一連のサブテストも理にかなっている可能性があります。
テーブル駆動テストと複数のテスト関数を組み合わせることができます。 たとえば、関数の非エラー出力が予期される出力と完全に一致することをテストし、関数が無効な入力を取得したときに非nilエラーを返すことをテストする場合、最も明確なユニットテストは、2つの別々のテーブル駆動テスト関数を作成することで実現できます。1つは通常の非エラー出力用、もう1つはエラー出力用です。
エラーのセマンティクスをテストする
ユニットテストで文字列比較を実行したり、`reflect.DeepEqual` を使用して特定の種類の入力が返されることを確認したりする場合、これらのエラーメッセージを今後変更する必要がある場合、テストが脆弱になる可能性があります。これは、ユニットテストを変更検出器に変えてしまう可能性があるため、文字列比較を使用して関数が返すエラーの種類を確認しないでください。
文字列比較を使用して、テスト対象のパッケージからのエラーメッセージが特定のプロパティ(たとえば、パラメータ名が含まれていること)を満たしていることを確認することは問題ありません。
関数が返すエラーの正確な型をテストする場合、人間が読むためのエラー文字列と、プログラムで使用するために公開されている構造を分離する必要があります。 この場合、セマンティックエラー情報を破壊する傾向がある `fmt.Errorf` の使用は避ける必要があります。
API を作成する多くの人は、API が異なる入力に対してどのような種類のエラーを返すかを正確には気にしません。 API がこのような場合は、`fmt.Errorf` を使用してエラーメッセージを作成し、ユニットテストでは、エラーが予期されたときにエラーが非nil であるかどうかのみをテストすれば十分です。
このコンテンツは Go Wiki の一部です。