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