onsi / gomega

Ginkgo's Preferred Matcher Library
http://onsi.github.io/gomega/
MIT License
2.16k stars 282 forks source link

Consider Generic Matchers #620

Open therealmitchconnors opened 1 year ago

therealmitchconnors commented 1 year ago

Have you considered adding generics support to gomega? I work with nested structures a lot in tests, and reflection-based checks are verbose, and may not work for types not easily castable from interface. Something like:

type Object struct {
  Owner Object
}

func getObjects() []Object {...}

func TestSomething(t *testing.T) {
  g := NewGomegaWithT(t)
  objs := getObjects()
  g.Expect(objs).To(HaveEach(
    func (x Object) bool {
      return x.Owner == nil
    }
  ))
}
onsi commented 1 year ago

I and others have explored using generics a bit - and while the compile-time feedback is certainly helpful, in practice I find that it doesn't solve as many problems and can make composition a bit harder.

Instead, Gomega has fully embraced reflection and been evolving towards some pretty nifty composition. As a concrete example, the test in your example can be written in the following ways:

type Object struct {
  Owner OtherObject
}

func getObjects() []Object {...}

// simple composition of matchers using `HaveField` and `BeNil`
func TestSomethingViaHaveField(t *testing.T) {
  g := NewGomegaWithT(t)
  g.Expect(getObjects()).To(HaveEach(HaveField("Owner", BeNil()))
}

//overkill for this example, but custom matchers are quite easy with `gcustom`
HaveNilOwner := gcustom.MakeMatcher(func(object Object) (bool, err) {
  return object.Owner == nil, nil
}).WithMessage("have nil owner)

func TestSomethingViaCustomMatcher(t *testing.T) {
  g := NewGomegaWithT(t)
  g.Expect(getObjects()).To(HaveEach(HaveNilOwner))
}

Note that in the gcustom example above, Gomega does the type assertions for you and will guarantee that your matcher function always receives an object of type Object (otherwise a clear error is emitted).

You can learn more about gcustom here and more about HaveField here. Finally, there are some patterns for building complex matchers via composition here - though the introduction of gcustom does make a lot of this stuff much easier now.