The testing packages provide utilities for comparing protocol buffer messages in tests, manually constructing wire-format data, and exercising protobuf reflection implementations. These tools are essential for writing robust tests for protobuf-related code.
import (
"testing"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/testing/protopack"
"google.golang.org/protobuf/testing/prototest"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"github.com/google/go-cmp/cmp"
)The protocmp package provides options for comparing protocol buffer messages with the go-cmp package, enabling detailed diff reporting in tests.
The primary feature that enables protobuf message comparison.
// Transform returns a cmp.Option that transforms proto.Message types into
// a comparable format for the cmp package
// All other options in this package must be used in conjunction with Transform
func Transform() cmp.OptionUsage example:
import (
"testing"
"google.golang.org/protobuf/testing/protocmp"
"github.com/google/go-cmp/cmp"
)
func TestMessage(t *testing.T) {
want := &pb.MyMessage{Name: "test", Value: 42}
got := &pb.MyMessage{Name: "test", Value: 42}
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf("MyMessage mismatch (-want +got):\n%s", diff)
}
}Options to ignore specific fields, messages, or descriptors during comparison.
// IgnoreFields ignores the specified fields in the specified message
// It is equivalent to FilterField(message, name, cmp.Ignore()) for each field
func IgnoreFields(message proto.Message, names ...protoreflect.Name) cmp.Option
// IgnoreMessages ignores all messages of the specified types
// It is equivalent to FilterMessage(message, cmp.Ignore()) for each message
func IgnoreMessages(messages ...proto.Message) cmp.Option
// IgnoreEnums ignores all enums of the specified types
// It is equivalent to FilterEnum(enum, cmp.Ignore()) for each enum
func IgnoreEnums(enums ...protoreflect.Enum) cmp.Option
// IgnoreDescriptors ignores the specified set of descriptors
// It is equivalent to FilterDescriptor(desc, cmp.Ignore()) for each descriptor
func IgnoreDescriptors(descs ...protoreflect.Descriptor) cmp.Option
// IgnoreOneofs ignores fields of the specified oneofs in the specified message
// It is equivalent to FilterOneof(message, name, cmp.Ignore()) for each oneof
func IgnoreOneofs(message proto.Message, names ...protoreflect.Name) cmp.Option
// IgnoreUnknown ignores unknown fields in all messages
func IgnoreUnknown() cmp.Option
// IgnoreEmptyMessages ignores messages that are empty or unpopulated
// It applies to standalone Message values, singular message fields, list fields
// of messages, and map fields of message values
func IgnoreEmptyMessages() cmp.Option
// IgnoreDefaultScalars ignores singular scalars that are unpopulated or
// explicitly set to the default value
// This option does not effect elements in a list or entries in a map
func IgnoreDefaultScalars() cmp.OptionUsage example:
// Ignore specific fields
diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.IgnoreFields(&pb.MyMessage{}, "timestamp", "id"),
)
// Ignore empty messages
diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.IgnoreEmptyMessages(),
)
// Ignore unknown fields
diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.IgnoreUnknown(),
)Options to filter comparisons to specific fields, messages, or descriptors.
// FilterField filters opt to only be applicable on the specified field in the message
// It panics if a field of the given name does not exist
//
// The Go type of the last path step may be an:
// - T for singular fields
// - []T for list fields
// - map[K]T for map fields
// - any for a Message map entry value
func FilterField(message proto.Message, name protoreflect.Name, opt cmp.Option) cmp.Option
// FilterMessage filters opt to only be applicable on a standalone Message values,
// singular fields of messages, list fields of messages, or map fields of message values,
// where the message is the same type as the specified message
//
// The Go type of the last path step may be an:
// - Message for singular fields, elements of a repeated field, values of a map field,
// or standalone Message values
// - []Message for list fields
// - map[K]Message for map fields
// - any for a Message map entry value
func FilterMessage(message proto.Message, opt cmp.Option) cmp.Option
// FilterEnum filters opt to only be applicable on a standalone Enum, singular fields of enums,
// list fields of enums, or map fields of enum values, where the enum is the same type
// as the specified enum
//
// The Go type of the last path step may be an:
// - Enum for singular fields, elements of a repeated field, values of a map field,
// or standalone Enum values
// - []Enum for list fields
// - map[K]Enum for map fields
// - any for a Message map entry value
func FilterEnum(enum protoreflect.Enum, opt cmp.Option) cmp.Option
// FilterDescriptor ignores the specified descriptor
//
// The following descriptor types may be specified:
// - protoreflect.EnumDescriptor
// - protoreflect.MessageDescriptor
// - protoreflect.FieldDescriptor
// - protoreflect.OneofDescriptor
func FilterDescriptor(desc protoreflect.Descriptor, opt cmp.Option) cmp.Option
// FilterOneof filters opt to only be applicable on all fields within the specified
// oneof in the message. It panics if a oneof of the given name does not exist
//
// The Go type of the last path step may be an:
// - T for singular fields
// - []T for list fields
// - map[K]T for map fields
// - any for a Message map entry value
func FilterOneof(message proto.Message, name protoreflect.Name, opt cmp.Option) cmp.OptionUsage example:
// Apply custom comparison only to specific field
diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.FilterField(&pb.MyMessage{}, "timestamp",
cmp.Comparer(func(a, b *timestamppb.Timestamp) bool {
// Custom timestamp comparison with tolerance
return math.Abs(float64(a.Seconds-b.Seconds)) < 1
}),
),
)Sort repeated fields for order-independent comparison.
// SortRepeated sorts repeated fields of the specified element type
// The less function must be of the form "func(T, T) bool" where T is the
// Go element type for the repeated field kind
//
// The element type T can be one of the following:
// - Go type for a protobuf scalar kind except for an enum
// (i.e., bool, int32, int64, uint32, uint64, float32, float64, string, and []byte)
// - E where E is a concrete enum type that implements protoreflect.Enum
// - M where M is a concrete message type that implement proto.Message
//
// This option only applies to repeated fields within a protobuf message
// It does not operate on higher-order Go types that seem like a repeated field
func SortRepeated(lessFunc any) cmp.OptionUsage example:
// Sort repeated int32 fields
diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.SortRepeated(func(a, b int32) bool { return a < b }),
)
// Sort repeated message fields
diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.SortRepeated(func(a, b *pb.Item) bool {
return a.Id < b.Id
}),
)The protopack package enables manual encoding and decoding of protobuf wire data for testing purposes. This is useful for creating test cases with precise control over wire encoding.
Represents a protobuf message as a sequence of tokens.
// Message is an ordered sequence of Token values
// It is functionally a concrete syntax tree that losslessly represents
// any arbitrary wire data (including invalid input)
type Message []Token
// Marshal encodes a syntax tree into the protobuf wire format
func (m Message) Marshal() []byte
// Size reports the size in bytes of the marshaled message
func (m Message) Size() int
// Unmarshal parses the input protobuf wire data as a syntax tree
// Any parsing error results in the remainder of the input being concatenated
// to the message as a Raw type
func (m *Message) Unmarshal(in []byte)
// UnmarshalDescriptor parses the input protobuf wire data as a syntax tree
// using the provided message descriptor for more accurate parsing of fields
func (m *Message) UnmarshalDescriptor(in []byte, desc protoreflect.MessageDescriptor)
// UnmarshalAbductive is like UnmarshalDescriptor, but infers abductively whether
// any unknown bytes values is a message based on whether it is a syntactically
// well-formed message
func (m *Message) UnmarshalAbductive(in []byte, desc protoreflect.MessageDescriptor)
// Format implements a custom formatter to visualize the syntax tree
// Using "%#v" formats the Message in Go source code
func (m Message) Format(s fmt.State, r rune)Individual wire format elements.
// Token is any other type (e.g., Message, Tag, Varint, Float32, etc)
type Token token
// Tag is a tuple of the field number and the wire type
type Tag struct {
Number Number
Type Type
}
// Varint is a signed varint using 64-bit two's complement encoding
type Varint int64
// Svarint is a signed varint using zig-zag encoding
type Svarint int64
// Uvarint is a unsigned varint
type Uvarint uint64
// Bool is a boolean
type Bool bool
// Int32 is a signed 32-bit fixed-width integer
type Int32 int32
// Uint32 is an unsigned 32-bit fixed-width integer
type Uint32 uint32
// Float32 is a 32-bit fixed-width floating point number
type Float32 float32
// Int64 is a signed 64-bit fixed-width integer
type Int64 int64
// Uint64 is an unsigned 64-bit fixed-width integer
type Uint64 uint64
// Float64 is a 64-bit fixed-width floating point number
type Float64 float64
// String is a length-prefixed string
type String string
// Bytes is a length-prefixed bytes
type Bytes []byte
// LengthPrefix is a length-prefixed message
type LengthPrefix Message
// Raw are bytes directly appended to output
type Raw []byte
// Denormalized is a denormalized varint value
// A varint is encoded using more bytes than is strictly necessary
type Denormalized struct {
Count uint // number of extra bytes
Value Token
}// Type is the wire type
type Type = protowire.Type
const (
VarintType Type = protowire.VarintType
Fixed32Type Type = protowire.Fixed32Type
Fixed64Type Type = protowire.Fixed64Type
BytesType Type = protowire.BytesType
StartGroupType Type = protowire.StartGroupType
EndGroupType Type = protowire.EndGroupType
)
// Number is the field number
type Number = protowire.Number
const (
MinValidNumber Number = protowire.MinValidNumber
FirstReservedNumber Number = protowire.FirstReservedNumber
LastReservedNumber Number = protowire.LastReservedNumber
MaxValidNumber Number = protowire.MaxValidNumber
)Usage example:
import (
"testing"
"google.golang.org/protobuf/testing/protopack"
"google.golang.org/protobuf/proto"
)
func TestWireFormat(t *testing.T) {
// Manually construct wire format data
wireData := protopack.Message{
protopack.Tag{1, protopack.BytesType}, protopack.String("Hello"),
protopack.Tag{2, protopack.VarintType}, protopack.Varint(42),
protopack.Tag{3, protopack.BytesType}, protopack.LengthPrefix{
protopack.Float32(1.1), protopack.Float32(2.2),
},
}.Marshal()
// Unmarshal and verify
msg := &pb.MyMessage{}
if err := proto.Unmarshal(wireData, msg); err != nil {
t.Fatal(err)
}
// Parse wire data as syntax tree
var m protopack.Message
m.Unmarshal(wireData)
t.Logf("Wire format: %#v", m)
}
func TestDenormalized(t *testing.T) {
// Create denormalized varint (using more bytes than necessary)
wireData := protopack.Message{
protopack.Tag{1, protopack.VarintType},
protopack.Denormalized{
Count: 3, // 3 extra bytes
Value: protopack.Varint(42),
},
}.Marshal()
// Test that decoder handles denormalized varints
msg := &pb.MyMessage{}
if err := proto.Unmarshal(wireData, msg); err != nil {
t.Fatal(err)
}
}The prototest package exercises protobuf reflection implementations to verify they work correctly.
Test a message implementation's reflection capabilities.
// Message tests a message implementation
type Message struct {
// Resolver is used to determine the list of extension fields to test with
// If nil, this defaults to using protoregistry.GlobalTypes
Resolver interface {
FindExtensionByName(field protoreflect.FullName) (protoreflect.ExtensionType, error)
FindExtensionByNumber(message protoreflect.FullName, field protoreflect.FieldNumber) (protoreflect.ExtensionType, error)
RangeExtensionsByMessage(message protoreflect.FullName, f func(protoreflect.ExtensionType) bool)
}
// UnmarshalOptions are respected for every Unmarshal call this package does
// The Resolver and AllowPartial fields are overridden
UnmarshalOptions proto.UnmarshalOptions
}
// Test performs tests on a protoreflect.MessageType implementation
func (test Message) Test(t testing.TB, mt protoreflect.MessageType)Test an enum implementation's reflection capabilities.
// Enum tests a protoreflect.EnumType implementation
type Enum struct{}
// Test performs tests on a protoreflect.EnumType implementation
func (test Enum) Test(t testing.TB, et protoreflect.EnumType)Usage example:
import (
"testing"
"google.golang.org/protobuf/testing/prototest"
)
func TestMyMessage(t *testing.T) {
// Test message reflection implementation
mt := (&pb.MyMessage{}).ProtoReflect().Type()
prototest.Message{}.Test(t, mt)
}
func TestMyEnum(t *testing.T) {
// Test enum reflection implementation
et := pb.MyEnum(0).Type()
prototest.Enum{}.Test(t, et)
}
func TestWithCustomResolver(t *testing.T) {
// Test with custom extension resolver
customResolver := &myCustomResolver{}
mt := (&pb.MyMessage{}).ProtoReflect().Type()
prototest.Message{
Resolver: customResolver,
}.Test(t, mt)
}func TestMessageEquality(t *testing.T) {
want := &pb.MyMessage{
Name: "test",
Items: []*pb.Item{{Id: 2}, {Id: 1}},
Metadata: map[string]string{"key": "value"},
}
got := &pb.MyMessage{
Name: "test",
Items: []*pb.Item{{Id: 1}, {Id: 2}},
Metadata: map[string]string{"key": "value"},
}
if diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.SortRepeated(func(a, b *pb.Item) bool { return a.Id < b.Id }),
); diff != "" {
t.Errorf("MyMessage mismatch (-want +got):\n%s", diff)
}
}func TestWithIgnoredFields(t *testing.T) {
want := &pb.MyMessage{
Name: "test",
Timestamp: timestamppb.Now(),
}
got := &pb.MyMessage{
Name: "test",
Timestamp: timestamppb.Now(), // Different timestamp
}
if diff := cmp.Diff(want, got,
protocmp.Transform(),
protocmp.IgnoreFields(&pb.MyMessage{}, "timestamp"),
); diff != "" {
t.Errorf("MyMessage mismatch (-want +got):\n%s", diff)
}
}func TestInvalidWireFormat(t *testing.T) {
// Create wire data with unknown field
wireData := protopack.Message{
protopack.Tag{1, protopack.BytesType}, protopack.String("test"),
protopack.Tag{999, protopack.VarintType}, protopack.Varint(42),
}.Marshal()
msg := &pb.MyMessage{}
if err := proto.Unmarshal(wireData, msg); err != nil {
t.Fatal(err)
}
// Verify unknown field is preserved
if len(msg.ProtoReflect().GetUnknown()) == 0 {
t.Error("Expected unknown fields to be preserved")
}
}These testing utilities provide comprehensive support for writing robust tests for protocol buffer code, enabling detailed comparisons, wire format validation, and reflection testing.