The gcustom package provides a simple mechanism for creating custom Gomega matchers with flexible failure message templates.
import "github.com/onsi/gomega/gcustom"The gcustom package allows you to build custom matchers from functions while providing powerful templating capabilities for failure messages. This is useful when the built-in Gomega matchers don't meet your specific needs.
func MakeMatcher(matchFunc any, args ...any) CustomGomegaMatcher{ .api }
Creates a custom Gomega-compatible matcher from a match function.
Match Function Requirements:
The matchFunc must:
(bool, error)Accepted Function Signatures:
Generic matcher - accepts any type:
func(actual any) (bool, error)Type-specific matcher - only accepts specific type (automatic type checking):
func(actual SpecificType) (bool, error)Optional Arguments:
string - Simple message string (equivalent to calling .WithMessage(message))*template.Template - Precompiled template (equivalent to calling .WithPrecompiledTemplate(template))Default Failure Messages:
When no custom message is provided, the matcher generates generic messages:
Positive failure (when Expect(actual).To(matcher) fails):
Custom matcher failed for:
<formatted actual>Negated failure (when Expect(actual).NotTo(matcher) fails):
Custom matcher succeeded (but was expected to fail) for:
<formatted actual>func ParseTemplate(templ string) (*template.Template, error){ .api }
Parses a template string for use with custom matcher failure messages. Use this when you need to precompile templates for performance or want to reuse templates across multiple matchers.
The parsed template includes a custom format function that uses Gomega's formatting:
{{format <object> <optional-indentation>}}type CustomGomegaMatcher struct {
// Internal fields - use MakeMatcher to construct
}{ .api }
A custom matcher created by MakeMatcher. Always construct using MakeMatcher rather than directly.
Methods:
func (c CustomGomegaMatcher) WithMessage(message string) CustomGomegaMatcher{ .api }
Configures the matcher with a simple message string. Produces failure messages like:
Positive failure:
Expected:
<formatted actual>
to <message>Negated failure:
Expected:
<formatted actual>
not to <message>func (c CustomGomegaMatcher) WithTemplate(templ string, data ...any) CustomGomegaMatcher{ .api }
Compiles and configures the matcher with a template string for failure messages. Optionally accepts a single data argument accessible in the template as {{.Data}}.
Template Variables:
{{.Failure}} - bool - true for positive failure, false for negated failure{{.NegatedFailure}} - bool - true for negated failure, false for positive failure{{.To}} - string - "to" for positive failure, "not to" for negated failure{{.Actual}} - any - the actual value passed to the matcher{{.FormattedActual}} - string - formatted actual value (indented by 1){{.Data}} - any - custom data passed via data argumentTemplate Functions:
{{format <object> <optional-indentation>}} - Formats an object using Gomega's default formattingfunc (c CustomGomegaMatcher) WithPrecompiledTemplate(templ *template.Template, data ...any) CustomGomegaMatcher{ .api }
Configures the matcher with a precompiled template (parsed using gcustom.ParseTemplate). Optionally accepts a single data argument accessible as {{.Data}}.
Use this for performance-critical code where template parsing overhead matters.
func (c CustomGomegaMatcher) WithTemplateData(data any) CustomGomegaMatcher{ .api }
Sets or updates the template data. The following are equivalent:
MakeMatcher(matchFunc).WithTemplate(templateString, data)
MakeMatcher(matchFunc).WithTemplate(templateString).WithTemplateData(data)func (c CustomGomegaMatcher) Match(actual any) (bool, error){ .api }
Implements the GomegaMatcher interface. Runs the match function and returns its result.
func (c CustomGomegaMatcher) FailureMessage(actual any) string{ .api }
Implements the GomegaMatcher interface. Generates the positive failure message (used when Expect(actual).To(matcher) fails).
func (c CustomGomegaMatcher) NegatedFailureMessage(actual any) string{ .api }
Implements the GomegaMatcher interface. Generates the negated failure message (used when Expect(actual).NotTo(matcher) fails).
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gcustom"
)
// Create a simple matcher that checks if a number is even
func BeEven() types.GomegaMatcher {
return gcustom.MakeMatcher(func(actual int) (bool, error) {
return actual%2 == 0, nil
}).WithMessage("be an even number")
}
// Usage
Expect(4).To(BeEven())
Expect(3).NotTo(BeEven())Failure output:
Expected:
<int>: 3
to be an even numbertype Machine struct {
Name string
Running bool
}
func BeRunning() types.GomegaMatcher {
return gcustom.MakeMatcher(func(m Machine) (bool, error) {
return m.Running, nil
}).WithMessage("be running")
}
// Usage
machine := Machine{Name: "Server", Running: true}
Expect(machine).To(BeRunning())If you pass the wrong type, the matcher automatically handles the error:
Expect("not a machine").To(BeRunning())
// Fails with: Matcher expected actual of type <Machine>. Got: <string>: not a machinefunc BeNilOrEmpty() types.GomegaMatcher {
return gcustom.MakeMatcher(func(actual any) (bool, error) {
if actual == nil {
return true, nil
}
v := reflect.ValueOf(actual)
switch v.Kind() {
case reflect.String, reflect.Slice, reflect.Map, reflect.Chan:
return v.Len() == 0, nil
default:
return false, fmt.Errorf("BeNilOrEmpty matcher expects nil, string, slice, map, or channel. Got: %T", actual)
}
}).WithMessage("be nil or empty")
}
// Usage
Expect([]int{}).To(BeNilOrEmpty())
Expect("").To(BeNilOrEmpty())
Expect(nil).To(BeNilOrEmpty())func HaveValidJSON() types.GomegaMatcher {
return gcustom.MakeMatcher(func(actual string) (bool, error) {
var js map[string]any
err := json.Unmarshal([]byte(actual), &js)
if err != nil {
return false, fmt.Errorf("invalid JSON: %w", err)
}
return true, nil
}).WithMessage("be valid JSON")
}
// When match returns an error, that error becomes the failure message
jsonStr := `{"broken": json`
Expect(jsonStr).To(HaveValidJSON())
// Fails with: invalid JSON: unexpected end of JSON inputtype Widget struct {
Name string
Version int
}
type Machine struct {
Name string
Widgets []Widget
}
func HaveWidget(widget Widget) types.GomegaMatcher {
return gcustom.MakeMatcher(func(machine Machine) (bool, error) {
for _, w := range machine.Widgets {
if w.Name == widget.Name && w.Version == widget.Version {
return true, nil
}
}
return false, nil
}).WithTemplate(
"Expected:\n{{.FormattedActual}}\n{{.To}} have widget named {{.Data.Name}}:\n{{format .Data 1}}",
widget,
)
}
// Usage
machine := Machine{
Name: "Factory",
Widgets: []Widget{
{Name: "gear", Version: 1},
},
}
Expect(machine).To(HaveWidget(Widget{Name: "sprocket", Version: 2}))Failure output:
Expected:
<Machine>: {
Name: "Factory",
Widgets: [
{Name: "gear", Version: 1},
],
}
to have widget named sprocket:
<Widget>: {Name: "sprocket", Version: 2}type Score struct {
Value int
Max int
}
func BePassingScore() types.GomegaMatcher {
return gcustom.MakeMatcher(func(score Score) (bool, error) {
passingScore := score.Max / 2
return score.Value >= passingScore, nil
}).WithTemplate(
`{{if .Failure}}` +
`Score of {{.Actual.Value}}/{{.Actual.Max}} is failing` +
` (need at least {{call .Divide .Actual.Max 2}})` +
`{{else}}` +
`Score of {{.Actual.Value}}/{{.Actual.Max}} is passing` +
` (but expected to fail)` +
`{{end}}`,
)
}
// Usage
Expect(Score{Value: 65, Max: 100}).To(BePassingScore())
Expect(Score{Value: 45, Max: 100}).NotTo(BePassingScore())var widgetTemplate = template.Must(
gcustom.ParseTemplate(
"Expected:\n{{.FormattedActual}}\n{{.To}} contain widget:\n{{format .Data 1}}",
),
)
func ContainWidget(widget Widget) types.GomegaMatcher {
return gcustom.MakeMatcher(func(widgets []Widget) (bool, error) {
for _, w := range widgets {
if w == widget {
return true, nil
}
}
return false, nil
}).WithPrecompiledTemplate(widgetTemplate, widget)
}
// Usage
widgets := []Widget{{Name: "gear", Version: 1}}
Expect(widgets).To(ContainWidget(Widget{Name: "gear", Version: 1}))// These are all equivalent ways to configure messages:
// 1. Pass message string as argument
matcher1 := gcustom.MakeMatcher(matchFunc, "do something")
// 2. Use WithMessage method
matcher2 := gcustom.MakeMatcher(matchFunc).WithMessage("do something")
// 3. Pass template as argument
templ := template.Must(gcustom.ParseTemplate("{{.To}} do something"))
matcher3 := gcustom.MakeMatcher(matchFunc, templ)
// 4. Use WithPrecompiledTemplate method
matcher4 := gcustom.MakeMatcher(matchFunc).WithPrecompiledTemplate(templ)
// 5. Template with data - inline
matcher5 := gcustom.MakeMatcher(matchFunc).WithTemplate("{{.To}} {{.Data}}", "something")
// 6. Template with data - separate calls
matcher6 := gcustom.MakeMatcher(matchFunc).
WithTemplate("{{.To}} {{.Data}}").
WithTemplateData("something")func HaveEvenLength() types.GomegaMatcher {
return gcustom.MakeMatcher(func(s string) (bool, error) {
return len(s)%2 == 0, nil
}).WithMessage("have even length")
}
// Use with And/Or combinators
Expect("test").To(And(
HaveLen(4),
HaveEvenLength(),
ContainSubstring("es"),
))
// Use with Eventually
Eventually(func() string {
return getData()
}).Should(HaveEvenLength())Type-specific matchers: When possible, use type-specific match functions for automatic type checking:
// Preferred - automatic type checking
func(m Machine) (bool, error)
// Less preferred - manual type checking required
func(actual any) (bool, error)Error handling: Return descriptive errors when matching fails for reasons other than the actual value not matching:
return gcustom.MakeMatcher(func(data string) (bool, error) {
parsed, err := parseData(data)
if err != nil {
return false, fmt.Errorf("failed to parse data: %w", err)
}
return parsed.Valid, nil
})Template performance: For matchers used in tight loops, precompile templates:
var myTemplate = template.Must(gcustom.ParseTemplate("..."))
func MyMatcher() types.GomegaMatcher {
return gcustom.MakeMatcher(matchFunc).WithPrecompiledTemplate(myTemplate)
}Clear messages: Write failure messages that clearly explain what was expected:
// Good
.WithMessage("have status code 200")
// Less clear
.WithMessage("be valid")Template data: Use template data to provide context about what was expected:
.WithTemplate(
"Expected {{.To}} contain {{.Data.Count}} items but found {{len .Actual}}",
struct{ Count int }{Count: expectedCount},
)