or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

core-assertions.mdgbytes.mdgcustom.mdgexec.mdghttp.mdgleak.mdgmeasure.mdgstruct.mdindex.mdmatchers.mdtypes.md
tile.json

gstruct.mddocs/

gstruct Package - Struct and Collection Field Matching

The gstruct package provides sophisticated matchers for deep inspection of structs, maps, and slices. It enables precise assertions about specific fields, keys, and elements while ignoring others, making it ideal for testing complex data structures.

Overview

Package: github.com/onsi/gomega/gstruct

The gstruct package is designed for matching complex nested data structures. It provides:

  • Flexible struct field matching with optional field checking
  • Map key-value matching with partial matching support
  • Slice/array element matching with identity-based selection
  • Pointer dereferencing for convenient pointer testing
  • Options to ignore extra or missing fields/keys/elements

Core Type Definitions

Fields

{ .api }

type Fields map[string]types.GomegaMatcher

Map of struct field names to matchers. Used with MatchFields and MatchAllFields.

Example:

gstruct.Fields{
    "Name": Equal("Alice"),
    "Age": BeNumerically(">", 18),
}

Keys

{ .api }

type Keys map[any]types.GomegaMatcher

Map of map keys to value matchers. Used with MatchKeys and MatchAllKeys.

Example:

gstruct.Keys{
    "status": Equal("active"),
    "count": BeNumerically(">=", 10),
}

Elements

{ .api }

type Elements map[string]types.GomegaMatcher

Map of element identifiers to matchers. Used with MatchElements and MatchAllElements.

Example:

gstruct.Elements{
    "alice": HaveField("Role", "admin"),
    "bob": HaveField("Role", "user"),
}

Identifier

{ .api }

type Identifier func(element any) string

Function that extracts a unique identifier from an element. Used with element matching functions.

Example:

func(element interface{}) string {
    return element.(User).ID
}

Options Types

{ .api }

type FieldsOptions int
type KeysOptions int
type ElementsOptions int

Option flags for controlling matching behavior.

FieldsOptions values:

  • IgnoreExtras - Ignore fields in the actual struct that aren't specified in Fields
  • IgnoreMissing - Ignore fields specified in Fields that don't exist in actual struct

KeysOptions values:

  • IgnoreExtras - Ignore keys in the actual map that aren't specified in Keys
  • IgnoreMissing - Ignore keys specified in Keys that don't exist in actual map

ElementsOptions values:

  • IgnoreExtras - Ignore elements in the actual slice that aren't specified in Elements
  • IgnoreMissing - Ignore elements specified in Elements that don't exist in actual slice
  • AllowDuplicates - Allow multiple elements with the same identifier

Struct Field Matchers

MatchAllFields

{ .api }

func MatchAllFields(fields Fields) types.GomegaMatcher

Matches if the actual struct has exactly the specified fields and all match. Fails if there are extra fields in the struct or missing fields.

Parameters:

  • fields - Map of field names to matchers

Returns: GomegaMatcher

Example:

type User struct {
    Name  string
    Age   int
    Email string
}

user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}

Expect(user).To(MatchAllFields(Fields{
    "Name":  Equal("Alice"),
    "Age":   Equal(30),
    "Email": Equal("alice@example.com"),
}))

// This would FAIL because "Email" field is not specified
Expect(user).To(MatchAllFields(Fields{
    "Name": Equal("Alice"),
    "Age":  Equal(30),
}))

MatchFields

{ .api }

func MatchFields(options FieldsOptions, fields Fields) types.GomegaMatcher

Matches specified struct fields with configurable behavior for extra or missing fields.

Parameters:

  • options - IgnoreExtras, IgnoreMissing, or combination
  • fields - Map of field names to matchers

Returns: GomegaMatcher

Examples:

type User struct {
    Name  string
    Age   int
    Email string
}

user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}

// Only check Name and Age, ignore Email
Expect(user).To(MatchFields(IgnoreExtras, Fields{
    "Name": Equal("Alice"),
    "Age":  Equal(30),
}))

// Check Name and try to check Phone (doesn't exist), but ignore missing
Expect(user).To(MatchFields(IgnoreMissing, Fields{
    "Name":  Equal("Alice"),
    "Phone": Equal("555-1234"), // This field doesn't exist, but ignored
}))

// Check some fields, ignore extras and missing
Expect(user).To(MatchFields(IgnoreExtras | IgnoreMissing, Fields{
    "Name":  Equal("Alice"),
    "Title": Equal("Engineer"), // Doesn't exist, ignored
}))

Map Matchers

MatchAllKeys

{ .api }

func MatchAllKeys(keys Keys) types.GomegaMatcher

Matches if the actual map has exactly the specified keys and all values match. Fails if there are extra keys in the map or missing keys.

Parameters:

  • keys - Map of keys to value matchers

Returns: GomegaMatcher

Example:

config := map[string]int{
    "timeout": 30,
    "retries": 3,
    "port":    8080,
}

Expect(config).To(MatchAllKeys(Keys{
    "timeout": Equal(30),
    "retries": Equal(3),
    "port":    Equal(8080),
}))

// This would FAIL because "port" is not specified
Expect(config).To(MatchAllKeys(Keys{
    "timeout": Equal(30),
    "retries": Equal(3),
}))

MatchKeys

{ .api }

func MatchKeys(options KeysOptions, keys Keys) types.GomegaMatcher

Matches specified map keys with configurable behavior for extra or missing keys.

Parameters:

  • options - IgnoreExtras, IgnoreMissing, or combination
  • keys - Map of keys to value matchers

Returns: GomegaMatcher

Examples:

config := map[string]interface{}{
    "host":     "localhost",
    "port":     8080,
    "timeout":  30,
    "database": "mydb",
}

// Only check host and port, ignore other keys
Expect(config).To(MatchKeys(IgnoreExtras, Keys{
    "host": Equal("localhost"),
    "port": Equal(8080),
}))

// Check for keys that may not exist
Expect(config).To(MatchKeys(IgnoreMissing, Keys{
    "host":     Equal("localhost"),
    "optional": Equal("value"), // Doesn't exist, but ignored
}))

// Flexible matching
Expect(config).To(MatchKeys(IgnoreExtras | IgnoreMissing, Keys{
    "host": Equal("localhost"),
}))

Slice and Array Element Matchers

MatchAllElements

{ .api }

func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher

Matches if the actual slice contains exactly the specified elements identified by the identifier function. Fails if there are extra or missing elements.

Parameters:

  • identifier - Function to extract unique identifier from each element
  • elements - Map of identifiers to element matchers

Returns: GomegaMatcher

Example:

type User struct {
    ID   string
    Name string
    Role string
}

users := []User{
    {ID: "1", Name: "Alice", Role: "admin"},
    {ID: "2", Name: "Bob", Role: "user"},
}

Expect(users).To(MatchAllElements(
    func(element interface{}) string {
        return element.(User).ID
    },
    Elements{
        "1": MatchFields(IgnoreExtras, Fields{
            "Name": Equal("Alice"),
            "Role": Equal("admin"),
        }),
        "2": MatchFields(IgnoreExtras, Fields{
            "Name": Equal("Bob"),
            "Role": Equal("user"),
        }),
    },
))

MatchElements

{ .api }

func MatchElements(identifier Identifier, options ElementsOptions, elements Elements) types.GomegaMatcher

Matches specified slice elements with configurable behavior for extra or missing elements.

Parameters:

  • identifier - Function to extract unique identifier from each element
  • options - IgnoreExtras, IgnoreMissing, AllowDuplicates, or combination
  • elements - Map of identifiers to element matchers

Returns: GomegaMatcher

Examples:

type Task struct {
    ID     string
    Status string
}

tasks := []Task{
    {ID: "task-1", Status: "pending"},
    {ID: "task-2", Status: "completed"},
    {ID: "task-3", Status: "failed"},
}

// Only check specific tasks, ignore others
Expect(tasks).To(MatchElements(
    func(element interface{}) string {
        return element.(Task).ID
    },
    IgnoreExtras,
    Elements{
        "task-1": MatchFields(IgnoreExtras, Fields{
            "Status": Equal("pending"),
        }),
        "task-2": MatchFields(IgnoreExtras, Fields{
            "Status": Equal("completed"),
        }),
    },
))

// Allow missing tasks
Expect(tasks).To(MatchElements(
    func(element interface{}) string {
        return element.(Task).ID
    },
    IgnoreMissing | IgnoreExtras,
    Elements{
        "task-1": MatchFields(IgnoreExtras, Fields{
            "Status": Equal("pending"),
        }),
        "task-99": MatchFields(IgnoreExtras, Fields{
            "Status": Equal("unknown"),
        }), // Doesn't exist, but ignored
    },
))

Pointer Matcher

PointTo

{ .api }

func PointTo(matcher types.GomegaMatcher) types.GomegaMatcher

Dereferences a pointer and applies the matcher to the pointed-to value. Fails if the pointer is nil.

Parameters:

  • matcher - Matcher to apply to dereferenced value

Returns: GomegaMatcher

Examples:

// Simple pointer matching
value := 42
ptr := &value
Expect(ptr).To(PointTo(Equal(42)))

// Struct pointer matching
type User struct {
    Name string
    Age  int
}

user := &User{Name: "Alice", Age: 30}

Expect(user).To(PointTo(MatchFields(IgnoreExtras, Fields{
    "Name": Equal("Alice"),
    "Age":  BeNumerically(">", 18),
})))

// Nested pointer matching
innerValue := 100
outerPtr := &innerValue
ptrToPtr := &outerPtr

Expect(ptrToPtr).To(PointTo(PointTo(Equal(100))))

Special Matchers

Ignore

{ .api }

func Ignore() types.GomegaMatcher

Matcher that always succeeds. Use this in Fields, Keys, or Elements maps to explicitly ignore certain fields/keys/elements while still requiring them to exist.

Returns: GomegaMatcher that always succeeds

Example:

type User struct {
    ID        string
    Name      string
    UpdatedAt time.Time
}

user := User{ID: "123", Name: "Alice", UpdatedAt: time.Now()}

// Check name but ignore UpdatedAt (don't care about its value)
Expect(user).To(MatchFields(IgnoreExtras, Fields{
    "Name":      Equal("Alice"),
    "UpdatedAt": Ignore(), // Field must exist but value is ignored
}))

Reject

{ .api }

func Reject() types.GomegaMatcher

Matcher that always fails. Use this to explicitly fail if a particular field/key/element is encountered.

Returns: GomegaMatcher that always fails

Example:

// Fail if a deprecated field is present
Expect(data).To(MatchKeys(IgnoreExtras, Keys{
    "newField":        Equal("value"),
    "deprecatedField": Reject(), // Fail if this key exists
}))

Element Matching with Index

MatchAllElementsWithIndex

{ .api }

func MatchAllElementsWithIndex(identifier IdentifierWithIndex, elements Elements) types.GomegaMatcher

Like MatchAllElements but the identifier function receives both the index and element.

Parameters:

  • identifier - Function that takes (index int, element any) and returns string
  • elements - Map of identifiers to element matchers

Returns: GomegaMatcher

Example:

items := []string{"apple", "banana", "cherry"}

Expect(items).To(MatchAllElementsWithIndex(
    func(idx int, element any) string {
        return fmt.Sprintf("item-%d", idx)
    },
    Elements{
        "item-0": Equal("apple"),
        "item-1": Equal("banana"),
        "item-2": Equal("cherry"),
    },
))

MatchElementsWithIndex

{ .api }

func MatchElementsWithIndex(identifier IdentifierWithIndex, options Options, elements Elements) types.GomegaMatcher

Like MatchElements but the identifier function receives both the index and element.

Parameters:

  • identifier - Function that takes (index int, element any) and returns string
  • options - Options (IgnoreExtras, IgnoreMissing, AllowDuplicates)
  • elements - Map of identifiers to element matchers

Returns: GomegaMatcher

Example:

tasks := []Task{
    {ID: "1", Status: "done"},
    {ID: "2", Status: "pending"},
    {ID: "3", Status: "failed"},
}

Expect(tasks).To(MatchElementsWithIndex(
    func(idx int, element any) string {
        return element.(Task).ID
    },
    IgnoreExtras,
    Elements{
        "1": MatchFields(IgnoreExtras, Fields{"Status": Equal("done")}),
    },
))

IndexIdentity

{ .api }

func IndexIdentity(index int, _ any) string

Helper function that returns the index as a string. Use with MatchElements when you want to match by position rather than a property.

Parameters:

  • index - Element index
  • _ - Element (unused)

Returns: String representation of index

Example:

items := []string{"first", "second", "third"}

Expect(items).To(MatchElements(IndexIdentity, IgnoreExtras, Elements{
    "0": Equal("first"),
    "2": Equal("third"),
}))

Usage Examples

Testing Complex Struct Hierarchies

type Address struct {
    Street  string
    City    string
    ZipCode string
}

type Person struct {
    Name    string
    Age     int
    Email   string
    Address Address
}

person := Person{
    Name:  "Alice",
    Age:   30,
    Email: "alice@example.com",
    Address: Address{
        Street:  "123 Main St",
        City:    "Springfield",
        ZipCode: "12345",
    },
}

// Match nested struct fields
Expect(person).To(MatchFields(IgnoreExtras, Fields{
    "Name": Equal("Alice"),
    "Age":  BeNumerically(">=", 18),
    "Address": MatchFields(IgnoreExtras, Fields{
        "City":    Equal("Springfield"),
        "ZipCode": MatchRegexp(`\d{5}`),
    }),
}))

Testing API Response Structures

type APIResponse struct {
    Status  string
    Message string
    Data    map[string]interface{}
    Meta    map[string]int
}

It("should return valid API response", func() {
    response := APIResponse{
        Status:  "success",
        Message: "User created successfully",
        Data: map[string]interface{}{
            "id":    "123",
            "name":  "Alice",
            "email": "alice@example.com",
        },
        Meta: map[string]int{
            "total": 1,
            "page":  1,
        },
    }

    Expect(response).To(MatchFields(IgnoreExtras, Fields{
        "Status": Equal("success"),
        "Data": MatchKeys(IgnoreExtras, Keys{
            "id":   Equal("123"),
            "name": Equal("Alice"),
        }),
        "Meta": MatchKeys(IgnoreExtras, Keys{
            "total": BeNumerically(">", 0),
        }),
    }))
})

Testing Collections of Structs

type Product struct {
    SKU   string
    Name  string
    Price float64
}

It("should return correct products", func() {
    products := []Product{
        {SKU: "ABC-123", Name: "Widget", Price: 19.99},
        {SKU: "DEF-456", Name: "Gadget", Price: 29.99},
        {SKU: "GHI-789", Name: "Doohickey", Price: 39.99},
    }

    // Check specific products by SKU
    Expect(products).To(MatchElements(
        func(element interface{}) string {
            return element.(Product).SKU
        },
        IgnoreExtras,
        Elements{
            "ABC-123": MatchFields(IgnoreExtras, Fields{
                "Name":  Equal("Widget"),
                "Price": BeNumerically("<", 20.0),
            }),
            "GHI-789": MatchFields(IgnoreExtras, Fields{
                "Name":  Equal("Doohickey"),
                "Price": BeNumerically(">", 30.0),
            }),
        },
    ))
})

Testing Partial Map Matching

It("should have required configuration keys", func() {
    config := map[string]interface{}{
        "host":         "localhost",
        "port":         8080,
        "debug":        true,
        "timeout":      30,
        "max_retries":  3,
        "buffer_size":  1024,
        "log_level":    "info",
    }

    // Only verify critical settings
    Expect(config).To(MatchKeys(IgnoreExtras, Keys{
        "host":    Equal("localhost"),
        "port":    BeNumerically(">", 1024),
        "timeout": BeNumerically(">=", 10),
    }))
})

Testing Pointers to Structs

type Config struct {
    Enabled bool
    Value   int
}

It("should handle pointer results", func() {
    config := &Config{
        Enabled: true,
        Value:   42,
    }

    Expect(config).To(PointTo(MatchFields(IgnoreExtras, Fields{
        "Enabled": BeTrue(),
        "Value":   Equal(42),
    })))
})

It("should handle nil pointers", func() {
    var config *Config = nil

    Expect(config).To(BeNil())
    // PointTo would fail on nil pointer
})

Testing Nested Collections

type Team struct {
    Name    string
    Members []string
}

It("should match teams with members", func() {
    teams := []Team{
        {Name: "Engineering", Members: []string{"Alice", "Bob"}},
        {Name: "Marketing", Members: []string{"Charlie", "Diana"}},
    }

    Expect(teams).To(MatchElements(
        func(element interface{}) string {
            return element.(Team).Name
        },
        IgnoreExtras,
        Elements{
            "Engineering": MatchFields(IgnoreExtras, Fields{
                "Members": ContainElements("Alice", "Bob"),
            }),
            "Marketing": MatchFields(IgnoreExtras, Fields{
                "Members": HaveLen(2),
            }),
        },
    ))
})

Testing with Composite Matchers

type Order struct {
    ID        string
    Status    string
    Total     float64
    Items     []string
    CreatedAt time.Time
}

It("should validate order structure", func() {
    order := Order{
        ID:        "ORD-001",
        Status:    "pending",
        Total:     199.99,
        Items:     []string{"item1", "item2"},
        CreatedAt: time.Now(),
    }

    Expect(order).To(MatchFields(IgnoreExtras, Fields{
        "ID":     MatchRegexp(`ORD-\d+`),
        "Status": Or(Equal("pending"), Equal("processing")),
        "Total":  And(
            BeNumerically(">", 0),
            BeNumerically("<", 10000),
        ),
        "Items":     Not(BeEmpty()),
        "CreatedAt": BeTemporally("~", time.Now(), time.Minute),
    }))
})

Testing Map with Different Value Types

It("should handle maps with interface{} values", func() {
    data := map[string]interface{}{
        "name":      "Alice",
        "age":       30,
        "active":    true,
        "balance":   1234.56,
        "tags":      []string{"premium", "verified"},
        "metadata":  map[string]string{"region": "us-west"},
    }

    Expect(data).To(MatchKeys(IgnoreExtras, Keys{
        "name":    Equal("Alice"),
        "age":     BeNumerically(">=", 18),
        "active":  BeTrue(),
        "balance": BeNumerically(">", 1000),
        "tags":    ContainElement("premium"),
        "metadata": MatchKeys(IgnoreExtras, Keys{
            "region": Equal("us-west"),
        }),
    }))
})

Testing Slice Elements with Custom Identifiers

type Event struct {
    Type      string
    Timestamp time.Time
    Data      map[string]interface{}
}

It("should match events by type", func() {
    events := []Event{
        {
            Type:      "user.created",
            Timestamp: time.Now(),
            Data:      map[string]interface{}{"user_id": "123"},
        },
        {
            Type:      "user.updated",
            Timestamp: time.Now(),
            Data:      map[string]interface{}{"user_id": "123", "field": "email"},
        },
    }

    Expect(events).To(MatchElements(
        func(element interface{}) string {
            return element.(Event).Type
        },
        IgnoreExtras,
        Elements{
            "user.created": MatchFields(IgnoreExtras, Fields{
                "Data": MatchKeys(IgnoreExtras, Keys{
                    "user_id": Equal("123"),
                }),
            }),
            "user.updated": MatchFields(IgnoreExtras, Fields{
                "Data": MatchKeys(IgnoreExtras, Keys{
                    "user_id": Equal("123"),
                    "field":   Equal("email"),
                }),
            }),
        },
    ))
})

Testing Optional Fields

type UserProfile struct {
    Username  string
    Email     string
    Phone     *string // Optional field
    Bio       *string // Optional field
}

It("should handle optional fields correctly", func() {
    phone := "555-1234"
    profile := UserProfile{
        Username: "alice",
        Email:    "alice@example.com",
        Phone:    &phone,
        Bio:      nil,
    }

    Expect(profile).To(MatchFields(IgnoreExtras, Fields{
        "Username": Equal("alice"),
        "Email":    ContainSubstring("@"),
        "Phone":    PointTo(MatchRegexp(`\d{3}-\d{4}`)),
        "Bio":      BeNil(),
    }))
})

Combining with Regular Matchers

type SearchResult struct {
    Query   string
    Results []string
    Count   int
    Page    int
}

It("should validate search results", func() {
    result := SearchResult{
        Query:   "golang testing",
        Results: []string{"result1", "result2", "result3"},
        Count:   100,
        Page:    1,
    }

    // Combine gstruct matchers with regular matchers
    Expect(result).To(SatisfyAll(
        MatchFields(IgnoreExtras, Fields{
            "Query":   ContainSubstring("golang"),
            "Results": HaveLen(3),
            "Count":   BeNumerically(">", 0),
        }),
        // Additional custom validation
        WithTransform(func(r SearchResult) int {
            return len(r.Results)
        }, BeNumerically("<=", 10)),
    ))
})

Testing Error Structures

type ValidationError struct {
    Field   string
    Message string
    Code    string
}

type ErrorResponse struct {
    Status string
    Errors []ValidationError
}

It("should return validation errors", func() {
    response := ErrorResponse{
        Status: "error",
        Errors: []ValidationError{
            {Field: "email", Message: "invalid format", Code: "INVALID_EMAIL"},
            {Field: "age", Message: "must be positive", Code: "INVALID_AGE"},
        },
    }

    Expect(response).To(MatchFields(IgnoreExtras, Fields{
        "Status": Equal("error"),
        "Errors": ContainElement(MatchFields(IgnoreExtras, Fields{
            "Field": Equal("email"),
            "Code":  Equal("INVALID_EMAIL"),
        })),
    }))
})

Best Practices

  1. Use IgnoreExtras for Forward Compatibility: When testing API responses, use IgnoreExtras to allow new fields to be added without breaking tests.

  2. Match What Matters: Only specify matchers for fields you care about, making tests less brittle.

  3. Use Descriptive Identifiers: For MatchElements, choose identifier functions that clearly represent element identity (e.g., ID, SKU, key).

  4. Combine with Other Matchers: gstruct matchers work seamlessly with all Gomega matchers for powerful assertions.

  5. Test Nested Structures: Don't hesitate to nest matchers deeply to validate complex hierarchical data.

  6. Handle Nil Pointers: Use BeNil() for nil pointers before using PointTo().

  7. Use MatchAllFields for Exact Matching: When you need to ensure no extra fields exist, use MatchAllFields or MatchAllKeys.

  8. Prefer IgnoreExtras for Partial Matching: Most tests should use IgnoreExtras to focus on relevant fields only.

Common Patterns

Testing Database Model Structs

var _ = Describe("User Repository", func() {
    It("should fetch user with correct structure", func() {
        user, err := repo.GetUser("123")
        Expect(err).NotTo(HaveOccurred())

        Expect(user).To(PointTo(MatchFields(IgnoreExtras, Fields{
            "ID":        Equal("123"),
            "Username":  Not(BeEmpty()),
            "Email":     MatchRegexp(`^[\w\.-]+@[\w\.-]+\.\w+$`),
            "CreatedAt": BeTemporally("~", time.Now(), 24*time.Hour),
        })))
    })
})

This comprehensive approach enables precise yet flexible testing of complex data structures at any level of nesting.