or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

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

gcustom.mddocs/

GCustom Package

The gcustom package provides a simple mechanism for creating custom Gomega matchers with flexible failure message templates.

Import

import "github.com/onsi/gomega/gcustom"

Overview

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.

Functions

MakeMatcher

func MakeMatcher(matchFunc any, args ...any) CustomGomegaMatcher

{ .api }

Creates a custom Gomega-compatible matcher from a match function.

Match Function Requirements:

The matchFunc must:

  • Take a single argument of any type
  • Return (bool, error)

Accepted Function Signatures:

  1. Generic matcher - accepts any type:

    func(actual any) (bool, error)
  2. 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>

ParseTemplate

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

Types

CustomGomegaMatcher

type CustomGomegaMatcher struct {
    // Internal fields - use MakeMatcher to construct
}

{ .api }

A custom matcher created by MakeMatcher. Always construct using MakeMatcher rather than directly.

Methods:

WithMessage

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>

WithTemplate

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 argument

Template Functions:

  • {{format <object> <optional-indentation>}} - Formats an object using Gomega's default formatting

WithPrecompiledTemplate

func (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.

WithTemplateData

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)

Match

func (c CustomGomegaMatcher) Match(actual any) (bool, error)

{ .api }

Implements the GomegaMatcher interface. Runs the match function and returns its result.

FailureMessage

func (c CustomGomegaMatcher) FailureMessage(actual any) string

{ .api }

Implements the GomegaMatcher interface. Generates the positive failure message (used when Expect(actual).To(matcher) fails).

NegatedFailureMessage

func (c CustomGomegaMatcher) NegatedFailureMessage(actual any) string

{ .api }

Implements the GomegaMatcher interface. Generates the negated failure message (used when Expect(actual).NotTo(matcher) fails).

Usage Examples

Basic Custom Matcher

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 number

Type-Safe Matcher

type 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 machine

Generic Matcher with Any Type

func 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())

Matcher with Error Handling

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 input

Template-Based Failure Messages

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

Advanced Template with Conditional Logic

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

Precompiled Template for Performance

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

Multiple Configuration Methods

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

Combining with Other Matchers

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

Best Practices

  1. 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)
  2. 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
    })
  3. 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)
    }
  4. Clear messages: Write failure messages that clearly explain what was expected:

    // Good
    .WithMessage("have status code 200")
    
    // Less clear
    .WithMessage("be valid")
  5. 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},
    )