or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

compiler-protogen.mdcore.mdencoding.mdindex.mdreflection.mdruntime.mdtesting.mdtypes-descriptorpb.mdtypes-dynamicpb.mdtypes-known.mdtypes-pluginpb.md
tile.json

testing.mddocs/

Testing Utilities

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.

Package Imports

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"
)

Protocol Buffer Comparison

The protocmp package provides options for comparing protocol buffer messages with the go-cmp package, enabling detailed diff reporting in tests.

Transform Option

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.Option

Usage 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)
    }
}

Ignore Options

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.Option

Usage 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(),
)

Filter Options

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.Option

Usage 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 Option

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.Option

Usage 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
    }),
)

Wire Format Construction

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.

Message Type

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)

Token Types

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
}

Wire Type Constants

// 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)
    }
}

Reflection Testing

The prototest package exercises protobuf reflection implementations to verify they work correctly.

Message Testing

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)

Enum Testing

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)
}

Common Testing Patterns

Comprehensive Message Comparison

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)
    }
}

Testing with Ignored Fields

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)
    }
}

Wire Format Testing

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.