viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方はこちらまで。
はじめに
皆様、初めまして。
viviON開発部 基盤チーム所属のniiと申します。
普段はマイクロサービスの開発や運用を行っており、その一環で詰まった部分について記事にしてみました。
何卒よろしくお願いします。
TL;DR
- protobuf比較にはcmpoptsではなくprotocmpを使おう
- protocmpにはcmpoptsのようにソート機能や特定のフィールドを無視する機能などがある
🤔 入れ子構造になっているものにソートが効かない
前提
下記のサンプルProtoファイルを用いて説明します!
UserとArticleが1対多の簡単なモデルです。
message Article{
int64 id = 1;
string title = 2;
string body = 3;
}
message User{
int64 id = 1;
string name = 2;
repeated Article articles = 3;
}
テスト
Golangにて、上記のUserやArticleを取得するrpcのテストを書きたい場合を考えてみます。
// イメージ
got := GetUserRPC()
if cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("diff: %v",diff)
}
ここで、GetUserRPC()
の返り値であるUser内のArticle順序は保証していない場合はどうなるでしょうか?(mapなどを用いて操作しており、順序が保証されないというケースなど)
got := protobuf.User{
Id: 1,
Name: "テストユーザー",
Articles: []*protobuf.Article{
// 順序がバラバラ
&article1,
&article4,
&article3,
&article2,
},
}
want := protobuf.User{
Id: 1,
Name: "テストユーザー",
Articles: []*protobuf.Article{
&article1,
&article2,
&article3,
&article4,
},
}
diff := cmp.Diff(want, got, protocmp.Transform())
diff: Service.User(Inverse(protocmp.Transform, protocmp.Message{
"@type": s"Service.User",
"articles": []protocmp.Message{
- s`{id:1, title:"サンプル記事1", body:"Hello World!"}`,
{"@type": s"Service.Article", "body": string("Hello World!"), "id": int64(2), "title": string("サンプル記事2")},
+ s`{id:1, title:"サンプル記事1", body:"Hello World!"}`,
+ s`{id:4, title:"サンプル記事4", body:"Hello World!"}`,
{"@type": s"Service.Article", "body": string("Hello World!"), "id": int64(3), "title": string("サンプル記事3")},
- s`{id:4, title:"サンプル記事4", body:"Hello World!"}`,
},
"id": int64(1),
"name": string("テストユーザー"),
}))
順序が違うため、当然ながら差分が出てしまっています。
順序が違うだけで入っているデータは同一であるため、
差分を検出しないように改修します。
今回の例ですと、ArticleのIdでSortしてあげればうまくいきそうです。
素直にcmpライブラリ内にあるcmpopts.SortSlices
を使用してみます。
// うまく動かない例
sortOpts := cmpopts.SortSlices(func(i, j *protobuf.Article) bool {
return i.Id < j.Id
})
diff := cmp.Diff(want, got, protocmp.Transform(), sortOpts)
ただしこれだと想定したように動かず、差分は消えません。
下記のようにArticle自体を比較すれば回避することもできますが、可能であればUserごとにまとめて確認したいところです。
cmp.Diff(want.Articles, got.Articles, protocmp.Transform(), sortOpts)
💡 解決策: protocmp.SortRepeated()を使おう
protocmpパッケージ内にprotocmp.SortRepeated
というソート機能がありました!
https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp#SortRepeated
ソート可能なのは下記の型です。
- protobufスカラー型(例: bool, int32, int64, uint32, uint64, float32, float64, string,[]byte)
- protoreflect.Enumを実装する具体的なenum型
- proto.Messageを実装する具体的なMessage型
今回はMessage型であるArticleをSortしたいため、下記のように記載します。
protoSortOpts := protocmp.SortRepeated(func(i, j *protobuf.Article) bool {
return i.Id < j.Id
})
diff = cmp.Diff(want, got, protocmp.Transform(), protoSortOpts)
diff: // 空文字
想定した動作通り、差分が出なくなりました!🎉
👀 その他の例:フィールドを無視したい場合
登録系RPCのテストを実施したい場合はどうでしょうか?
登録したUserを返すStoreUserRPC()の結果を比較してみます。
// イメージ
got := StoreUserRPC(input)
if cmp.Diff(want, got, protocmp.Transform(), protoSortOpts); diff != "" {
t.Errorf("diff: %v",diff)
}
登録RPCを実行する前はIdが不明なので、一旦wantデータのIdは0にします。
want := protobuf.User{
Id: 0,
Name: "テストユーザー",
Articles: []*protobuf.Article{
&article1,
&article2,
&article3,
&article4,
},
}
ただこのままテストを実行すると、当然ながらIdの差分が検出されます。
+ "id": int64(100), // idは登録されるまでわからない
今回は特定のフィールドを無視するオプションを使ってあげれば良さそうです。
ここまで読んだ皆様ならもうお分かりですね。
今回もcmpopts.IgnoreFields
ではなく、 protocmp.IgnoreFields
を使ってあげましょう!
先ほど指定してあげたprotocmp.SortRepeated
と重ねて使用することもできます。
optsList := []cmp.Option{
protocmp.Transform(),
protocmp.SortRepeated(func(i, j *protobuf.Article) bool {
return i.Id < j.Id
}),
protocmp.IgnoreFields(&protobuf.User{}, "id"),
}
diff := cmp.Diff(want, got, optsList...)
diff: // 空文字
さいごに
ここまで読んでいただきありがとうございました!
protoの比較にはprotocmpを使う事を頭の片隅に置いていただけると幸いです。
それでは良きprotobufライフを!
一緒に二次元業界を盛り上げていきませんか?
株式会社viviONでは、マイクロサービスを推進するエンジニアを募集しています。
また、マイクロサービスに限らず、バックエンド・フロント・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。