Goのテストで文字列のポインタ型の値に対する比較をする時に迷った話
文字列のポインタ型の値を比較するテストコード
Goのテストで、次のような文字列のポインタ型の値に対する比較とその結果を表示するコードを書きました。
このテストコードでは、以下のことを検証しています。
- 関数Fがnilを返さない
- 関数Fがnilを返さなかった場合、結果のポインタ型の値が指し示す内容が期待する文字列と一致する
func F() *string {
s := "value"
return &s
}
func TestF(t *testing.T) {
got := F()
want := "value"
if got == nil || want != *got {
t.Errorf("want: %v, got: %v", want, got)
}
}
実際にテストを実行すると、問題なく成功して終了します。
しかしながら、以下のケースではどうでしょうか?
- 関数Fからnilが返ったケース
- 関数Fから期待しない文字列が返ったケース
それぞれのケースについて確認してみましょう。
1. 関数Fからnilが返ったケース
下記のように、関数Fがnilを返すように書き換えます。
func F() *string {
return nil
}
func TestF(t *testing.T) {
got := F()
want := "value"
// got == nil で引っ掛かる
if got == nil || want != *got {
t.Errorf("want: %v, got: %v", want, got)
}
}
この時、テスト結果の出力内容は次のようになります。
=== RUN TestF
prog_test.go:14: want: value, got: <nil>
--- FAIL: TestF (0.00s)
FAIL
got: <nil>
という表示は期待通りで、特に問題なく動作することが確認できました。
2. 関数Fから期待しない文字列が返ったケース
下記のように、関数Fが文字列 "invalid value"
のポインタを返すように書き換えます。
func F() *string {
s := "invalid value"
return &s
}
func TestF(t *testing.T) {
got := F()
want := "value"
// want != *got で引っ掛かる
if got == nil || want != *got {
t.Errorf("want: %v, got: %v", want, got)
}
}
この時、テスト結果の出力内容は次のようになります。
=== RUN TestF
prog_test.go:15: want: value, got: 0xc0001161d0
--- FAIL: TestF (0.00s)
FAIL
got: 0xc0001161d0
という表示は、明らかに期待通りではありません。
このテストでは、結果として具体的にどのような文字列が返って失敗したのかを確認したいので、別の方法を使って結果を出力した方がよいことがわかりました。
対応策
対応策として、以下のものを検討しました。
- t.Errorfのフォーマット方法を変える
- if文の条件を分ける
- 結果の出力時にpointer indirectionする
- reflect.DeepEqualを使う
- cmp.Diffを使う
それぞれについて、検討の結果を示します。
t.Errorfのフォーマット方法を変える
今使っている %v
で十分な情報が出せていないのではと思い、fmt packageのドキュメントを参考に別の書式を検討しました。
%v the value in a default format
when printing structs, the plus flag (%+v) adds field names
%#v a Go-syntax representation of the value
%s the uninterpreted bytes of the string or slice
ドキュメントから読み取れる通り、今回のような文字列のポインタ型の値の出力を詳細に行うというオプションはなく、うまくいかないとは思いつつも下記のコードで検証を行いました。
func main() {
s := "abcde"
ptr := &s
var nilPtr *string
fmt.Printf("%v, %v\n", ptr, nilPtr)
fmt.Printf("%+v, %+v\n", ptr, nilPtr)
fmt.Printf("%#v, %#v\n", ptr, nilPtr)
fmt.Printf("%s, %s\n", ptr, nilPtr)
}
結果としては以下のようになり、いずれも期待する出力にはなりませんでした。
0xc000102020, <nil>
0xc000102020, <nil>
(*string)(0xc000102020), (*string)(nil)
%!s(*string=0xc000102020), %!s(*string=<nil>)
if文の条件を分ける
このパターンは、一番素朴で、かつ明らかにうまくいきます。
func F() *string {
s := "invalid value"
return &s
}
func TestF(t *testing.T) {
got := F()
want := "value"
if got == nil {
t.Errorf("want: %v, got: %v", nil, got)
} else if want != *got {
t.Errorf("want: %v, got: %v", want, *got)
}
}
出力結果は以下の通りです。
=== RUN TestF
prog_test.go:17: want: value, got: invalid value
--- FAIL: TestF (0.00s)
FAIL
しかしながら、if ~ elseの分岐をこの目的で書くとなると、コードの冗長性が増えてしまうのでやや気になるところです。
結果の出力時にpointer indirectionする
このパターンは、明らかにうまくいきません。
関数Fが文字列のポインタ型の値を返す時はうまく動きますが、nilを返した時はpanicします。
func F() *string {
return nil
}
func TestF(t *testing.T) {
got := F()
want := "value"
if got == nil || want != *got {
// *gotでpanic!
t.Errorf("want: %v, got: %v", want, *got)
}
}
出力結果は以下の通りです。
=== RUN TestF
--- FAIL: TestF (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x504c67]
reflect.DeepEqualを使う
reflect.DeepEqualは、ポインタ型の変数の指し示す内容も含めた比較を行ってくれます。
このパターンは、if文の条件を単純化するのには使えますが、結局結果の出力の問題は解決できません。
func F() *string {
return nil
}
func TestF(t *testing.T) {
got := F()
wantVal := "value"
want := &wantVal
if !reflect.DeepEqual(want, got) {
// *gotでpanic
t.Errorf("want: %v, got: %v", want, *got)
}
}
cmp.Diffを使う
最後に試したのがこのパターンです。
今回のテスト対象のプロジェクトでは、 go-cmpを導入していたので、これを使います。
冒頭の2パターンのそれぞれについて、go-cmpを使った場合の結果を見てみましょう。
1. 関数Fからnilが返ったケース
func F() *string {
return nil
}
func TestF(t *testing.T) {
got := F()
wantVal := "value"
want := &wantVal
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("(-want +got):\n%s", diff)
}
}
出力結果は以下の通りで、nilが返ってきたことが明確にわかる形式となっています。
=== RUN TestF
prog_test.go:18: (-want +got):
(*string)(
- &"value",
+ nil,
)
--- FAIL: TestF (0.00s)
FAIL
2. 関数Fから期待しない文字列が返ったケース
func F() *string {
s := "invalid value"
return &s
}
func TestF(t *testing.T) {
got := F()
wantVal := "value"
want := &wantVal
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("(-want +got):\n%s", diff)
}
}
出力結果は以下の通りで、nilの場合と同様非常に見やすい形式です。
=== RUN TestF
prog_test.go:19: (-want +got):
&string(
- "value",
+ "invalid value",
)
--- FAIL: TestF (0.00s)
FAIL
今回の結論
結論としては、cmp.Diffを使うのが最も楽で、かつ出力結果も見やすいのでこちらを使うことにしました。
個人的には、cmp.Diffは複雑なフィールド構成の構造体の比較に使うことが多かったのですが、今回、単純なポインタ型の値の比較で使っても便利ということがわかったので、使い方の幅を広げてみようと思います。
外部のライブラリに依存せずに、今回の目的を単純な形で達成できる方法がもしあれば、コメントなどでぜひ教えてください!
ヒューマンエラーを減らせる管理画面を数分で立ち上げるローコードSaaS「ベースマキナ」の公式アカウントです。サービスサイトはこちらから。 about.basemachina.com
Discussion