1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

viviONAdvent Calendar 2024

Day 19

protobufをcmp.Diffする際はcmpoptsではなくprotocmpを使おう

Last updated at Posted at 2024-12-18

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・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?