[インデックス 13201] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージ内の decode_test.go
ファイルに対する変更です。このファイルは、JSONデータのデコード(Goのデータ構造へのアンマーシャリング)機能が正しく動作するかを検証するための単体テストを含んでいます。具体的には、json.Unmarshal
関数の様々な入力パターンと期待される出力、およびエラーケースをテストしています。
コミット
encoding/json: Unmarshalにラウンドトリップテストを追加
また、テーブルをタグ付きリテラルを使用するように変換。
R=golang-dev, bradfitz CC=golang-dev https://golang.org/cl/6258061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d61707f49053d13afb7c9bfdb7981aeff94a9c62
元コミット内容
commit d61707f49053d13afb7c9bfdb7981aeff94a9c62
Author: Russ Cox <rsc@golang.org>
Date: Tue May 29 18:02:40 2012 -0400
encoding/json: add round trip test in Unmarshal
Also convert table to use tagged literal.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6258061
変更の背景
このコミットは、encoding/json
パッケージの Unmarshal
関数のテストの堅牢性を向上させることを目的としています。
-
テストの網羅性向上(ラウンドトリップテスト): 従来のテストでは、JSON文字列をGoのデータ構造にアンマーシャルする際の正確性のみを検証していました。しかし、アンマーシャルされたデータが、再度JSONにマーシャル(エンコード)された際に元のJSON文字列と等価な表現になるか、あるいは元のGoのデータ構造と等価なJSON表現になるか、という「ラウンドトリップ」の検証は行われていませんでした。このラウンドトリップテストを追加することで、
Unmarshal
とMarshal
の両関数が連携して正しく機能すること、およびデータ変換の可逆性が保証されるようになります。これは、データの整合性を保つ上で非常に重要です。 -
テストコードの可読性と保守性の向上(タグ付きリテラル): 既存のテストケースは、
unmarshalTest
構造体のフィールドに値を順番に渡す形式で記述されていました。この形式は、構造体のフィールドの順序が変更された場合にテストコードも修正する必要があるなど、保守性が低いという問題がありました。また、各値がどのフィールドに対応するのかが直感的に分かりにくいという可読性の問題もありました。タグ付きリテラル(field: value
の形式)を使用することで、これらの問題を解決し、テストケースの意図をより明確にし、将来的な変更に対する耐性を高めることができます。
前提知識の解説
Go言語の encoding/json
パッケージ
Go言語の encoding/json
パッケージは、JSON (JavaScript Object Notation) 形式のデータとGoのデータ構造(構造体、スライス、マップなど)の間で変換を行うための機能を提供します。
json.Marshal
: Goのデータ構造をJSON形式のバイトスライスにエンコード(マーシャル)します。json.Unmarshal
: JSON形式のバイトスライスをGoのデータ構造にデコード(アンマーシャル)します。
これらの関数は、WebアプリケーションでのAPI通信や設定ファイルの読み込みなど、GoアプリケーションでJSONデータを扱う上で不可欠です。
reflect
パッケージと reflect.DeepEqual
Go言語の reflect
パッケージは、実行時にプログラムの構造を検査・操作するための機能を提供します。リフレクションは、型情報、フィールド、メソッドなどを動的に取得・操作する際に使用されます。
reflect.DeepEqual(x, y interface{}) bool
: 2つの引数x
とy
が「深く」等しいかどうかを再帰的に比較します。これは、プリミティブ型だけでなく、構造体、スライス、マップなどの複合型についても、その内容が完全に一致するかどうかを検証する際に非常に便利です。単なる==
演算子では参照の比較しかできない場合があるため、値の比較にはDeepEqual
がよく用いられます。テストにおいて、期待される出力と実際の結果が複雑なデータ構造である場合に、その内容が完全に一致するかを確認するために頻繁に利用されます。
Go言語のテーブル駆動テスト (Table-Driven Tests)
Go言語では、複数のテストケースを効率的に記述するために「テーブル駆動テスト」というパターンがよく用いられます。これは、テスト対象の関数に与える入力と、それに対応する期待される出力(および期待されるエラーなど)を構造体のスライスとして定義し、そのスライスをループで回しながら各テストケースを実行する手法です。
利点:
- 簡潔性: 多数のテストケースをコンパクトに記述できます。
- 可読性: 各テストケースの入力と期待される出力が一覧で分かりやすくなります。
- 保守性: 新しいテストケースの追加や既存のテストケースの変更が容易になります。
Go言語の構造体リテラルとタグ付きリテラル
Go言語では、構造体のインスタンスを生成する際に「構造体リテラル」を使用します。
-
通常の構造体リテラル: フィールドの値を定義された順序で記述します。
type Person struct { Name string Age int } p := Person{"Alice", 30} // フィールド名なし
この形式は、構造体のフィールドの順序が変更されると、コンパイルエラーになったり、意図しない値が代入されたりするリスクがあります。
-
タグ付き構造体リテラル (Keyed Struct Literals): フィールド名を明示的に指定して値を記述します。
p := Person{Name: "Alice", Age: 30} // フィールド名あり
この形式は、フィールドの順序に依存しないため、構造体の定義が変更されてもテストコードを修正する必要が少なく、可読性も向上します。このコミットでは、テストケースの
unmarshalTest
構造体の初期化にこのタグ付きリテラルが導入されています。
技術的詳細
ラウンドトリップテストの導入
このコミットの主要な変更点の一つは、TestUnmarshal
関数内に「ラウンドトリップテスト」のロジックが追加されたことです。
ラウンドトリップテストの概念:
ラウンドトリップテストとは、ある形式のデータを別の形式に変換し、その変換されたデータを元の形式に戻したときに、元のデータと変換後のデータが等価であることを検証するテスト手法です。
encoding/json
の文脈では、以下のステップで実行されます。
- JSON文字列 (
tt.in
) をGoのデータ構造 (v.Interface()
) にUnmarshal
する。 Unmarshal
されたGoのデータ構造 (v.Interface()
) を再度JSON形式 (enc
) にMarshal
する。Marshal
されたJSON (enc
) を、新しいGoのデータ構造 (vv.Interface()
) にUnmarshal
する。- 最初の
Unmarshal
で得られたGoのデータ構造 (v.Elem().Interface()
) と、2回目のUnmarshal
で得られたGoのデータ構造 (vv.Elem().Interface()
) がreflect.DeepEqual
で完全に一致するかを検証する。
なぜ重要か: このテストは、単にJSONからGoへの変換が正しいだけでなく、Goのデータ構造がJSONとして正しく表現され、さらにそのJSONがGoのデータ構造として正しく解釈されるという、双方向の変換の整合性を保証します。これにより、データの損失や予期せぬ変換エラーを防ぎ、APIの相互運用性やデータの永続化における信頼性を高めることができます。例えば、あるGoプログラムがJSONを読み込み、それを別のGoプログラムにJSONとして渡すようなシナリオで、データの整合性が保たれることを保証します。
タグ付きリテラルへの変換
unmarshalTests
スライス内の unmarshalTest
構造体の初期化方法が、従来の順序ベースのリテラルから、フィールド名を明示的に指定する「タグ付きリテラル」に変換されました。
変更前:
{`true`, new(bool), true, nil},
この形式では、unmarshalTest
構造体のフィールド定義が変更された場合(例: フィールドの追加、削除、順序変更)、このテストケースもすべて修正する必要がありました。また、true
が in
フィールドに対応し、new(bool)
が ptr
フィールドに対応するといった対応関係が、構造体の定義を知らないと直感的に理解しにくいという問題がありました。
変更後:
{in: `true`, ptr: new(bool), out: true},
この形式では、in:
, ptr:
, out:
, err:
といったフィールド名が明示的に指定されています。これにより、各値がどのフィールドに割り当てられているかが一目で分かり、コードの可読性が大幅に向上します。また、unmarshalTest
構造体のフィールドの順序が変更されても、このテストケース自体を修正する必要がなくなるため、保守性も向上します。これは、Goのテストコードのベストプラクティスの一つです。
コアとなるコードの変更箇所
このコミットでは、src/pkg/encoding/json/decode_test.go
ファイルが変更されています。
-
unmarshalTests
スライス内のunmarshalTest
構造体リテラルの変更:- 約61行目から100行目にかけて、
unmarshalTests
スライス内の各テストケースの記述が、フィールド名を明示的に指定するタグ付きリテラル形式に変更されています。 - 例:
- 変更前:
{
true, new(bool), true, nil},
- 変更後:
{in:
true, ptr: new(bool), out: true},
- 変更前:
- エラーを期待するテストケースでは、
err:
フィールドが追加されています。- 例:
- 変更前:
{
{"X": [1,2,3], "Y": 4}, new(T), T{Y: 4}, &UnmarshalTypeError{"array", reflect.TypeOf("")}},
- 変更後:
{in:
{"X": [1,2,3], "Y": 4}, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeOf("")}},
- 変更前:
- 例:
- 約61行目から100行目にかけて、
-
TestUnmarshal
関数内でのラウンドトリップテストの追加:- 約170行目から188行目にかけて、
TestUnmarshal
関数のループ内に新しいコードブロックが追加されています。 - このブロックは、
tt.err == nil
(エラーが期待されないテストケース) の場合にのみ実行されます。 - 追加されたロジックは以下の通りです。
Marshal(v.Interface())
でGoのデータ構造をJSONにマーシャル。Unmarshal(enc, vv.Interface())
でマーシャルされたJSONを新しいGoのデータ構造にアンマーシャル。reflect.DeepEqual(v.Elem().Interface(), vv.Elem().Interface())
で、元のGoのデータ構造と、ラウンドトリップ後に得られたGoのデータ構造が等しいか比較。
- 約170行目から188行目にかけて、
コアとなるコードの解説
unmarshalTests
の変更
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -61,50 +61,50 @@ type unmarshalTest struct {
var unmarshalTests = []unmarshalTest{
// basic types
- {`true`, new(bool), true, nil},
- {`1`, new(int), 1, nil},
- {`1.2`, new(float64), 1.2, nil},
- {`-5`, new(int16), int16(-5), nil},
- {`"a\u1234"`, new(string), "a\u1234", nil},
- {`"http:\/\/`, new(string), "http://", nil},
- {`"g-clef: \uD834\uDD1E"`, new(string), "g-clef: \U0001D11E", nil},
- {`"invalid: \uD834x\uDD1E"`, new(string), "invalid: \uFFFDx\uFFFD", nil},
- {"null", new(interface{}), nil, nil},
- {`{"X": [1,2,3], "Y": 4}`, new(T), T{Y: 4}, &UnmarshalTypeError{"array", reflect.TypeOf("")}},
- {`{"x": 1}`, new(tx), tx{}, &UnmarshalFieldError{"x", txType, txType.Field(0)}},
+ {in: `true`, ptr: new(bool), out: true},
+ {in: `1`, ptr: new(int), out: 1},
+ {in: `1.2`, ptr: new(float64), out: 1.2},
+ {in: `-5`, ptr: new(int16), out: int16(-5)},
+ {in: `\"a\\u1234\"`, ptr: new(string), out: \"a\\u1234\"},
+ {in: `\"http:\\/\\/\"`, ptr: new(string), out: \"http://\"},
+ {in: `\"g-clef: \\uD834\\uDD1E\"`, ptr: new(string), out: \"g-clef: \\U0001D11E\"},
+ {in: `\"invalid: \\uD834x\\uDD1E\"`, ptr: new(string), out: \"invalid: \\uFFFDx\\uFFFD\"},
+ {in: \"null\", ptr: new(interface{}), out: nil},
+ {in: `{\"X\": [1,2,3], \"Y\": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{\"array\", reflect.TypeOf(\"\")}},
+ {in: `{\"x\": 1}`, ptr: new(tx), out: tx{}, err: &UnmarshalFieldError{\"x\", txType, txType.Field(0)}},
この変更は、unmarshalTest
構造体の各フィールド(in
, ptr
, out
, err
)に明示的に名前を付けて値を割り当てる「タグ付きリテラル」形式に移行したものです。これにより、テストケースの可読性が向上し、unmarshalTest
構造体のフィールドの順序が変更されても、テストコードを修正する必要がなくなります。特に、err:
フィールドが追加されたことで、エラーを期待するテストケースの意図がより明確になりました。
TestUnmarshal
関数へのラウンドトリップテストの追加
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -170,6 +170,24 @@ func TestUnmarshal(t *testing.T) {
println(string(data))\n \t\t\tcontinue\n \t\t}\n+\n+\t\t// Check round trip.\n+\t\tif tt.err == nil {\n+\t\t\tenc, err := Marshal(v.Interface())\n+\t\t\tif err != nil {\n+\t\t\t\tt.Errorf(\"#%d: error re-marshaling: %v\", i, err)\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\tvv := reflect.New(reflect.TypeOf(tt.ptr).Elem())\n+\t\t\tif err := Unmarshal(enc, vv.Interface()); err != nil {\n+\t\t\t\tt.Errorf(\"#%d: error re-unmarshaling: %v\", i, err)\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\tif !reflect.DeepEqual(v.Elem().Interface(), vv.Elem().Interface()) {\n+\t\t\t\tt.Errorf(\"#%d: mismatch\\nhave: %#+v\\nwant: %#+v\", i, v.Elem().Interface(), vv.Elem().Interface())\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t}\n \t}\n }\n \n```
このコードブロックは、`TestUnmarshal` 関数内の各テストケースの検証後に追加されています。
1. `if tt.err == nil`: このラウンドトリップテストは、元のテストケースがエラーを期待しない場合にのみ実行されます。エラーが期待されるケースでは、そもそも `Unmarshal` が成功しないため、ラウンドトリップテストは意味がありません。
2. `enc, err := Marshal(v.Interface())`: 最初の `Unmarshal` で得られたGoのデータ構造 `v` を、`json.Marshal` を使ってJSONバイトスライス `enc` に再マーシャルします。ここでエラーが発生した場合、テストは失敗します。
3. `vv := reflect.New(reflect.TypeOf(tt.ptr).Elem())`: 新しいGoのデータ構造 `vv` を作成します。これは、元のテストケースで `Unmarshal` のターゲットとして使用された型 (`tt.ptr` の要素型) と同じ型を持つポインタです。
4. `if err := Unmarshal(enc, vv.Interface()); err != nil`: 再マーシャルされたJSON `enc` を、新しく作成した `vv` に `json.Unmarshal` します。ここでエラーが発生した場合も、テストは失敗します。
5. `if !reflect.DeepEqual(v.Elem().Interface(), vv.Elem().Interface())`: 最後に、最初の `Unmarshal` で得られたGoのデータ構造 (`v.Elem().Interface()`) と、ラウンドトリップ後に得られたGoのデータ構造 (`vv.Elem().Interface()`) が `reflect.DeepEqual` を使って比較されます。もし両者が等しくない場合、つまりラウンドトリップでデータが変化してしまった場合、テストは失敗し、詳細な不一致情報が出力されます。
この追加により、`Unmarshal` のテストがより包括的になり、`Marshal` との連携におけるデータの整合性も検証されるようになりました。
## 関連リンク
* Go言語 `encoding/json` パッケージ公式ドキュメント: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)
* Go言語 `reflect` パッケージ公式ドキュメント: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
* Go言語のテストに関する公式ドキュメント: [https://go.dev/doc/code#testing](https://go.dev/doc/code#testing)
## 参考にした情報源リンク
* Go言語 `encoding/json` パッケージのソースコード (`decode_test.go`): [https://github.com/golang/go/blob/master/src/encoding/json/decode_test.go](https://github.com/golang/go/blob/master/src/encoding/json/decode_test.go)
* Go言語のコミット履歴 (GitHub): [https://github.com/golang/go/commits/master](https://github.com/golang/go/commits/master)
* Go言語のコードレビューシステム (Gerrit): [https://go-review.googlesource.com/c/go/+/6258061](https://go-review.googlesource.com/c/go/+/6258061) (コミットメッセージに記載されている `golang.org/cl/6258061` のリンク)
* Go言語のテーブル駆動テストに関する一般的な情報源 (例: Go by Example - Table Driven Tests): [https://gobyexample.com/table-driven-tests](https://gobyexample.com/table-driven-tests)
* Go言語の構造体リテラルに関する情報源 (例: Effective Go - Struct Literals): [https://go.dev/doc/effective_go#struct_literals](https://go.dev/doc/effective_go#struct_literals)
* Go言語の `reflect.DeepEqual` に関する情報源 (例: GoDoc - reflect.DeepEqual): [https://pkg.go.dev/reflect#DeepEqual](https://pkg.go.dev/reflect#DeepEqual)