Composing Gomega Matchers

Introducing new features in this popular Go testing library.

Announcing New Gomega Matchers

Gomega is a Golang library for writing lovely matcher/assertion statements in your unit tests. For example:

Expect(color).To(Equal("red"))

This rich syntax also allows Gomega to display an informative error message when a test fails.

While Gomega has provided a wealth of primitive matchers such as BeTrue() or HaveLen(...), it could use a little more power – a way to compose matchers into larger expressions.

After a little elbow grease, I’m happy to announce four new matchers are now available in the Gomega library: And, Or, Not and WithTransform. Together, they provide a powerful way to compose existing matchers together into complex expressions.

Let’s take a brief look at the new matchers:

And, also named SatisfyAll, returns true only if all of the provided matchers return true.

Expect(number).To(SatisfyAll(
    BeNumerically(">", 0),
    BeNumerically("<", 10)))

Or, also named SatisfyAny, returns true only if at least one of the provided matchers return true.

Expect(number).To(SatisfyAny(
    Equal(5),
    Equal(10))

Not negates the provided matcher, returning true only if the matcher returns false.

Expect(msg).To(Not(BeEmpty()))
// Yes, we could have just used `ToNot(...)` instead of `To(Not(...))`.
// But in nested expressions, `Not(...)` becomes an indispensable operator,
// as we'll see later.

WithTransform lets you transform the value in some way before passing it to the given matcher.

getName := func(p Person) string { return p.FirstName }

Expect(person).To(WithTransform(getName, Equal("Roger")))

But we could also create a lightweight matcher – a simple function that composes existing matchers. That is a powerful use case enabled by WithTransform and the other new matchers. So the above example would become:

// create a lightweight matcher
func HaveFirstName(n string) GomegaMatcher {
	return WithTransform(func(p Person) string { return p.FirstName }, Equal(n))
}

// the matcher statement is simpler:
Expect(person).To(HaveFirstName("Roger"))

For more information refer to the Composing Matchers section of the Gomega documentation.

Concluding Example

I leave you with one slightly ridiculous example, but it demonstrates what’s possible if necessary. Imagine you want to verify that a fetchPerson() function eventually returns one of multiple acceptable Person values.

// Hypothetical Person struct
type Person struct {
	FirstName string
	LastName  string
	Age       int
	FetchedAt time.Time
}

When we compare two Person structs, we want to only consider certain fields. We don’t care about the FetchedAt timestamp because that is somewhat random/unpredictable. So let’s create some lightweight field matchers:

// lightweight matchers for Person fields
func HaveFirstName(n string) types.GomegaMatcher {
	return WithTransform(func(p Person) string { return p.FirstName }, Equal(n))
}
func HaveLastName(n string) types.GomegaMatcher {
	return WithTransform(func(p Person) string { return p.LastName }, Equal(n))
}
func HaveAge(ageMatcher types.GomegaMatcher) types.GomegaMatcher {
	// ageMatcher gives callers flexibility to use Equal, BeNumerically, etc.
	return WithTransform(func(p Person) int { return p.Age }, ageMatcher)
}

We are now empowered to write complicated statements, such as this contrived example:

// fetchPerson() should eventually return either a "John Doe", or a non-Smith,
// or someone older than 65.
Eventually(fetchPerson).Should(
	Or(
		And(HaveFirstName("John"), HaveLastName("Doe")),
		Not(HaveLastName("Smith")),
		HaveAge(BeNumerically(">", 65))))

Hopefully you never have to write such convoluted tests, but it’s nice to know you could go there if you needed to.

Have fun testing your code with Gomega!