Golang Unit testing with Gomega matcher

eye-catch Golang

A function result somehow needs to be compared in a unit test to know whether the function returns the expected value or not. There are multiple modules that provide assertions to make it work for unit testing. I use gomega in my work and it’s a beautiful tool that I highly recommend for unit testing. It’s a preferred framework for Ginkgo that is a BDD testing framework.

In this post, I will introduce you how to use gomega matcher.

If you try to use it, install both Ginkgo and Gomega.

Sponsored links

Gomega format to assert the result

The basic usage of gomega matcher is as follows. Put the result from a function call to Expect function. Then, chain one of the function calls listed below depending on the expected result. To and Should have the same meaning. We can use the preferred one. It accepts matchers in it where we can write the expected result.

Expect(result).To(matcherFunction)
Expect(result).ToNot(matcherFunction)
Expect(result).Should(matcherFunction)
Expect(result).ShouldNot(matcherFunction)

A matcher is for example Equal function. It expects exactly the same value.

Expect(result).To(Equal(expectedValue))

Expect is a main function that we most often use but gomega provides Eventually and Consistently functions too. They can be used for an asynchronous function call. This usage and the difference will be explained later.

Sponsored links

How to validate Number

Let’s see the actual examples. It’s a number validation first.

Exact type and value match

If the result is int64, expected value must be int64 too. int64(1) == 1 becomes false. Therefore, the first line uses ShouldNot. The value must match the expected value but the type must match as well.

Expect(int64(1)).ShouldNot(Equal(1))
Expect(int64(1)).Should(Equal(int64(1)))

Ignore type

Use BeNumerically matcher if we want to check only the value. It ignores the data type.

floatingValue := 1.0
Expect(floatingValue).ShouldNot(Equal(1))
Expect(floatingValue).Should(Equal(1.0))
Expect(floatingValue).Should(Equal(float64(1)))
Expect(floatingValue).Should(BeNumerically("==", 1)) // ignore the data type

Range match

There might be some cases where we don’t need to check the value strictly but want to check whether the value is in the specific range.

Expect(5).Should(BeNumerically(">", 2))
Expect(5).Should(BeNumerically(">=", 5))
Expect(5).Should(BeNumerically("<", 50))

If the result is a floating value, it sometimes becomes 1.00000001 due to the nature of the bit calculations. If you want to accept the deviation, we can write it in the following way. The third parameter is the range that we want to accept.

Expect(1.001).Should(BeNumerically("~", 1.0, 0.01))
Expect(1.001).ShouldNot(BeNumerically("~", 1.0, 0.0001))

How to validate Bool

It’s easy. Equal matcher can be used but it’s readable to use BeTrue or BeFalse.

Expect(true).Should(Equal(true))
Expect(true).Should(BeTrue())
Expect(false).Should(BeFalse())

How to validate nil

We must be aware that Equal matcher can’t be used for nil. We must always use BeNil for nil.

var nilValue *string
Expect(nilValue).ShouldNot(Equal(nil))
Expect(nilValue).Should(BeNil())

How to validate Array

Let’s validate an empty array first. There are multiple ways to validate it.

emptyArray := []int{}
Expect(emptyArray).ShouldNot(BeNil())   // Not nil
Expect(emptyArray).Should(BeEmpty())
Expect(emptyArray).Should(HaveLen(0))
Expect(len(emptyArray)).Should(BeZero())

var nilArray []int
Expect(nilArray).Should(BeNil())    // nil
Expect(nilArray).Should(BeEmpty())
Expect(nilArray).Should(HaveLen(0))
Expect(len(nilArray)).Should(BeZero())

the emptyArray is defined with 0 length. Therefore, it’s not nil. BeNil matcher shouldn’t be used for array. Let’s see another example.

array5 := make([]int, 5)
Expect(array5).ShouldNot(BeNil())
Expect(array5).ShouldNot(BeEmpty())
Expect(array5).Should(HaveLen(5))
Expect(len(array5)).Should(Equal(5))

This array is defined with no value but the size is already defined; and thus, it’s not empty. We should check the length if we want to check if no element is added to the array.

Let’s validate whether the expected values are included at last. If using Equal matcher, the array must have the same values in the same order.

array := []int{5, 4, 3, 2, 1}
Expect(array).Should(HaveLen(5))
Expect(array).Should(Equal([]int{5, 4, 3, 2, 1}))
Expect(array).ShouldNot(Equal([]int{1, 2, 3, 4, 5}))

Contain some elements

The order is not always important. Use ContainElements matcher in this case.

Expect(array).Should(ContainElements([]int{4, 2, 5}))
Expect(array).Should(ContainElements(1, 3, 2))
Expect(array).Should(And(
    ContainElement(1),
    ContainElement(3),
    ContainElement(2),
    ContainElement(BeNumerically("<", 5)),
))

The first one is an array while the second one is 3 integers. I prefer the second way because it’s shorter. If a bit of complex validation is needed, we can write it in the third way. It validates if the array has 1, 2, 3, and all values are less than 5. It fails if the array contains 6,

Array struct contains a specific data

How can we write the expected value if the array is struct? Basically, it’s the same as above. The order matters if Equal is used. Use ContainElement or ContainElements to check if the array contains the expected data set.

keyValues := []KeyValue{
    {Key: "key1", Value: "val1"},
    {Key: "key2", Value: "val2"},
    {Key: "key3", Value: "val3"},
}
Expect(keyValues).Should(HaveLen(3))
Expect(keyValues).Should(Equal([]KeyValue{
    {Key: "key1", Value: "val1"},
    {Key: "key2", Value: "val2"},
    {Key: "key3", Value: "val3"},
}))
Expect(keyValues).Should(ContainElement(KeyValue{Key: "key2", Value: "val2"}))
Expect(keyValues).Should(ContainElements(
    KeyValue{Key: "key2", Value: "val2"},
    KeyValue{Key: "key3", Value: "val3"},
))

If we want to validate only one of the struct properties, we can write it in the following way.

Expect(keyValues).Should(ContainElement(HaveField("Key", Equal("key2"))))
Expect(keyValues).Should(ContainElements(
    HaveField("Key", MatchRegexp("k..2")),
    HaveField("Key", Equal("key3")),
))

Since we can specify any matcher, regex can also be possible to use as you can see in the second validation.

How to validate Map

Use BeEmpty or check the length when we want to validate that no data is assigned to the map.

emptyMap := map[string]int{}
Expect(len(emptyMap)).Should(BeZero())
Expect(emptyMap).Should(BeEmpty())
Expect(emptyMap).ShouldNot(BeNil()) // it's not nil
Expect(emptyMap).Should(HaveLen(0))

var nilMap map[string]int
Expect(len(nilMap)).Should(BeZero())
Expect(nilMap).Should(BeEmpty())
Expect(nilMap).Should(BeNil())
Expect(nilMap).Should(HaveLen(0))

Map doesn’t have order in it because it’s hash-based. So we can use Equal matcher and we don’t have to write the expected values in the same order as the actual data.

mapValue := map[string]int{"first": 1, "second": 2, "third": 3}
Expect(mapValue).Should(HaveLen(3))
Expect(mapValue).Should(Equal(map[string]int{"first": 1, "second": 2, "third": 3}))
Expect(mapValue).Should(Equal(map[string]int{"second": 2, "first": 1, "third": 3}))

Use HaveKey or HaveKeyWithValue when validating that the map contains the specific key or key-value pair.

Expect(mapValue).Should(HaveKey("second"))
Expect(mapValue).Should(HaveKeyWithValue("second", 2))

By the way, the following code doesn’t pass for some reason…

// This fails for some reason...
Expect(mapValue).Should(ContainElements(
    HaveKey("second"),
    HaveKeyWithValue("second", 2),
    HaveKeyWithValue("third", 3),
))

Write the assertion in a separate lines if you want to use HaveKey or HaveKeyWithValue.

Expect(mapValue).Should(HaveKey("second"))
Expect(mapValue).Should(HaveKeyWithValue("second", 2))
Expect(mapValue).Should(HaveKeyWithValue("third", 3))

How to validate Error

The simplest way for error validation is using HaveOccurred. It validates whether a function returns an error or not. If the error message is needed, call Error() function and validate the string.

var errFoo error = errors.New("foo error")

Expect(errFoo).Should(HaveOccurred())
Expect(errFoo.Error()).Should(ContainSubstring("foo err"))

Error type check

If we want to validate the specific error type, we can use errors.Is, errors.As, or MatchError matcher. As you can see the example, errors.Is can also be used for a wrapped error.

Expect(errors.Is(errFoo, errFoo)).Should(BeTrue())
Expect(errFoo).Should(MatchError(errFoo))
Expect(errFoo).Should(MatchError(ContainSubstring("foo err")))

wrappedErr := fmt.Errorf("additional error info here: %w", errFoo)
Expect(errors.Is(wrappedErr, errFoo)).Should(BeTrue())
Expect(wrappedErr).Should(MatchError(errFoo))

If the error is a custom error type, we need to define Is() function to make errors.Is() work. Go to the following link for the details.

Custom error type check

Let’s define the following custom error. It doesn’t have any property.

type ErrorWithoutProp struct{}

func (e ErrorWithoutProp) Error() string {
    return "error message from ErrorWithoutProp"
}

If it doesn’t have any property, it’s easy to validate it. We can use Is(), As(), and MatchError even if it’s a wrapped error.

err := &ErrorWithoutProp{}
Expect(errors.Is(err, &ErrorWithoutProp{})).Should(BeTrue())

var expectedErr *ErrorWithoutProp
Expect(errors.As(err, &expectedErr)).Should(BeTrue())

Expect(err).Should(MatchError(&ErrorWithoutProp{}))

wrappedErr := fmt.Errorf("additional error info here: %w", err)
Expect(errors.Is(wrappedErr, &ErrorWithoutProp{})).Should(BeTrue())
Expect(errors.As(wrappedErr, &expectedErr)).Should(BeTrue())
Expect(wrappedErr).Should(MatchError(&ErrorWithoutProp{}))

Let’s check a custom error type with a property next.

type ErrorWithProp struct {
    Name string
}

func (e ErrorWithProp) Error() string {
    return "error message from ErrorWithProp"
}

Be aware that the Is() function returns false here because the custom error doesn’t implement Is() function.

err := &ErrorWithProp{Name: "Yuto"}
Expect(errors.Is(err, &ErrorWithProp{})).Should(BeFalse())
Expect(errors.Is(err, &ErrorWithProp{Name: "Yuto"})).Should(BeFalse())

var expectedErr *ErrorWithProp
Expect(errors.As(err, &expectedErr)).Should(BeTrue())

Expect(err).Should(MatchError(&ErrorWithProp{Name: "Yuto"}))

wrappedErr := fmt.Errorf("additional error info here: %w", err)
Expect(errors.As(wrappedErr, &expectedErr)).Should(BeTrue())

Expect(wrappedErr).Should(MatchError(&ErrorWithProp{Name: "Yuto"}))

Is() is not necessary to implement if it’s not used in the production code. So using MatchError might be good to stick.

How to validate if Channel receives a data

If we need to validate channel, we shouldn’t use <-channel because it blocks the thread and it might not end the unit test. Use Eventually or Consistently instead. If it doesn’t fulfill the expected result, it throws an error as timeout.

Consistently check the result

Consistently polls the state every 10ms by default. The channel doesn’t receive anything for 500ms in the following code. Therefore, Consistently validates the behavior. It blocks 500ms and poll the channel state every 10ms. It fails if we change the timeout from 500ms to 550ms.

channel := make(chan int, 5)
go func(c chan int) {
    time.Sleep(500 * time.Millisecond)
    c <- 1
}(channel)

Consistently(channel, "500ms").ShouldNot(Receive())

It can be used if we want to validate that the channel doesn’t receive anything for the specific duration.

We can use Consistently if there is a function that returns the current state of something. This function just counts up when it’s called. It’s called every ms and thus the result remains smaller than 30.

count := 0
getNum := func() int {
    count++
    return count
}

Consistently(getNum, "30ms").
    WithPolling(time.Millisecond). // default 10ms
    Should(BeNumerically("<", 30))

Note that the function is not called 30 times in this test because it takes more than 1 ms to execute the function.

Eventually receives something

Eventually validates that the channel eventually sends data. The following code validates that the channel receives data in 600 ms.

channel := make(chan int, 5)
go func(c chan int) {
    time.Sleep(500 * time.Millisecond)
    c <- 1
}(channel)

Eventually(channel).
    WithTimeout(600 * time.Millisecond). // default 1 sec
    Should(Receive())

If the value needs to be validated, add matcher(s) to Receive mather.

channel := make(chan int, 5)
go func(c chan int) {
    time.Sleep(100 * time.Millisecond)
    c <- 1
    time.Sleep(100 * time.Millisecond)
    c <- 2
    time.Sleep(100 * time.Millisecond)
    c <- 3
}(channel)

Eventually(channel).Should(Receive())
Eventually(channel).Should(Receive(Equal(2)))
Eventually(channel).Should(Receive(Equal(3)))

If the channel is struct and the property needs to be validated, an additional step is required. The detail is written in the following post.

Comments

Copied title and URL