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.
Package: github.com/onsi/gomega/gstruct
The gstruct package is designed for matching complex nested data structures. It provides:
{ .api }
type Fields map[string]types.GomegaMatcherMap of struct field names to matchers. Used with MatchFields and MatchAllFields.
Example:
gstruct.Fields{
"Name": Equal("Alice"),
"Age": BeNumerically(">", 18),
}{ .api }
type Keys map[any]types.GomegaMatcherMap of map keys to value matchers. Used with MatchKeys and MatchAllKeys.
Example:
gstruct.Keys{
"status": Equal("active"),
"count": BeNumerically(">=", 10),
}{ .api }
type Elements map[string]types.GomegaMatcherMap of element identifiers to matchers. Used with MatchElements and MatchAllElements.
Example:
gstruct.Elements{
"alice": HaveField("Role", "admin"),
"bob": HaveField("Role", "user"),
}{ .api }
type Identifier func(element any) stringFunction that extracts a unique identifier from an element. Used with element matching functions.
Example:
func(element interface{}) string {
return element.(User).ID
}{ .api }
type FieldsOptions int
type KeysOptions int
type ElementsOptions intOption flags for controlling matching behavior.
FieldsOptions values:
IgnoreExtras - Ignore fields in the actual struct that aren't specified in FieldsIgnoreMissing - Ignore fields specified in Fields that don't exist in actual structKeysOptions values:
IgnoreExtras - Ignore keys in the actual map that aren't specified in KeysIgnoreMissing - Ignore keys specified in Keys that don't exist in actual mapElementsOptions values:
IgnoreExtras - Ignore elements in the actual slice that aren't specified in ElementsIgnoreMissing - Ignore elements specified in Elements that don't exist in actual sliceAllowDuplicates - Allow multiple elements with the same identifier{ .api }
func MatchAllFields(fields Fields) types.GomegaMatcherMatches 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 matchersReturns: 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),
})){ .api }
func MatchFields(options FieldsOptions, fields Fields) types.GomegaMatcherMatches specified struct fields with configurable behavior for extra or missing fields.
Parameters:
options - IgnoreExtras, IgnoreMissing, or combinationfields - Map of field names to matchersReturns: 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
})){ .api }
func MatchAllKeys(keys Keys) types.GomegaMatcherMatches 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 matchersReturns: 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),
})){ .api }
func MatchKeys(options KeysOptions, keys Keys) types.GomegaMatcherMatches specified map keys with configurable behavior for extra or missing keys.
Parameters:
options - IgnoreExtras, IgnoreMissing, or combinationkeys - Map of keys to value matchersReturns: 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"),
})){ .api }
func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcherMatches 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 elementelements - Map of identifiers to element matchersReturns: 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"),
}),
},
)){ .api }
func MatchElements(identifier Identifier, options ElementsOptions, elements Elements) types.GomegaMatcherMatches specified slice elements with configurable behavior for extra or missing elements.
Parameters:
identifier - Function to extract unique identifier from each elementoptions - IgnoreExtras, IgnoreMissing, AllowDuplicates, or combinationelements - Map of identifiers to element matchersReturns: 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
},
)){ .api }
func PointTo(matcher types.GomegaMatcher) types.GomegaMatcherDereferences a pointer and applies the matcher to the pointed-to value. Fails if the pointer is nil.
Parameters:
matcher - Matcher to apply to dereferenced valueReturns: 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)))){ .api }
func Ignore() types.GomegaMatcherMatcher 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
})){ .api }
func Reject() types.GomegaMatcherMatcher 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
})){ .api }
func MatchAllElementsWithIndex(identifier IdentifierWithIndex, elements Elements) types.GomegaMatcherLike MatchAllElements but the identifier function receives both the index and element.
Parameters:
identifier - Function that takes (index int, element any) and returns stringelements - Map of identifiers to element matchersReturns: 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"),
},
)){ .api }
func MatchElementsWithIndex(identifier IdentifierWithIndex, options Options, elements Elements) types.GomegaMatcherLike MatchElements but the identifier function receives both the index and element.
Parameters:
identifier - Function that takes (index int, element any) and returns stringoptions - Options (IgnoreExtras, IgnoreMissing, AllowDuplicates)elements - Map of identifiers to element matchersReturns: 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")}),
},
)){ .api }
func IndexIdentity(index int, _ any) stringHelper 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"),
}))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}`),
}),
}))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),
}),
}))
})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),
}),
},
))
})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),
}))
})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
})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),
}),
},
))
})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),
}))
})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"),
}),
}))
})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"),
}),
}),
},
))
})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(),
}))
})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)),
))
})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"),
})),
}))
})Use IgnoreExtras for Forward Compatibility: When testing API responses, use IgnoreExtras to allow new fields to be added without breaking tests.
Match What Matters: Only specify matchers for fields you care about, making tests less brittle.
Use Descriptive Identifiers: For MatchElements, choose identifier functions that clearly represent element identity (e.g., ID, SKU, key).
Combine with Other Matchers: gstruct matchers work seamlessly with all Gomega matchers for powerful assertions.
Test Nested Structures: Don't hesitate to nest matchers deeply to validate complex hierarchical data.
Handle Nil Pointers: Use BeNil() for nil pointers before using PointTo().
Use MatchAllFields for Exact Matching: When you need to ensure no extra fields exist, use MatchAllFields or MatchAllKeys.
Prefer IgnoreExtras for Partial Matching: Most tests should use IgnoreExtras to focus on relevant fields only.
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.