Improving Your Go Tests and Mocks With Testify
🚀 My new course - The Golang Testing Bible is out now and covers everything you need to get up and running creating tests for your Go applications!
Assertions are something that I genuinely feel the standard library in Go is missing. You can most definitely achieve the same results with the likes of if
comparisons and whatever else, but it’s not the cleanest way to write your test files.
This is where the likes of stretchr/testify comes in to save the day. This package has quickly become one of the most popular testing packages, if not the most popular testing package for Go developers around the world.
Its elegant syntax allows you to write incredibly easy assertions that just make sense.
Getting Started
The first thing we’ll have to do in order to get up and running with the testify package is to install it. Now, if you are using Go Modules then this will just be a case of calling go test ...
after importing the package at the top of one of your *_test.go
files.
However, if you are still stuck on an older version of Go, you can get this package by typing:
go get github.com/stretchr/testify
After you have done this, we should be good to start incorporating it into our various testing suites.
A Simple Example
Let’s start off by looking at how we would traditionally write tests in Go. This should give us a good idea of what testify
brings to the table in terms of improved readability.
We’ll start by defining a really simple Go program that features one exported function, Calculate()
.
package main import ( "fmt" ) // Calculate returns x + 2. func Calculate(x int) (result int) { result = x + 2 return result } func main() { fmt.Println("Hello World") }
If we were to write tests for this using traditional methods, we would typically end up with something like this:
package main import ( "testing" ) func TestCalculate(t *testing.T) { if Calculate(2) != 4 { t.Error("Expected 2 + 2 to equal 4") } }
We can then try run this simple test by calling go test ./... -v
, passing in the -v
flag to ensure we can see a more verbose output.
If we wanted to be a bit fancier, we might incorporate table-driven tests in here to ensure a wide variety of cases were tested. For now though, let’s try and modify this basic approach to see how testify
works:
package main import ( "testing" "github.com/stretchr/testify/assert" ) func TestCalculate(t *testing.T) { assert.Equal(t, Calculate(2), 4) }
Awesome, as you can see, we’ve managed to succinctly test for equality using the assert.Equal
function. Straight away this looks like an improvement as we’ve got fewer lines of code to read over and we can clearly see what the test function is trying to achieve.
Negative Test Cases and Nil Tests
So, we’ve looked at happy path testing, but how about negative assertions and Nil checks. Well, thankfully the testify
package has methods that allow us to test for both.
Say we wanted to test a function that returns a status of a given application. For example, if the application was alive and waiting for requests then the status would return "waiting"
, if it had crashed, then it would return "down"
as well as a variety of other statuses for when it’s serving a request, or when it’s waiting on a third party, etc.
When we perform our test, we would want our test to pass as long as the status equaled anything but "down"
, so we could use assert.NotEqual()
in this particular, hypothetical case.
func TestStatusNotDown(t *testing.T) { assert.NotEqual(t, status, "down") }
If we wanted to test to see if "status"
was not nil then we could use either assert.Nil(status)
or assert.NotNil(object)
depending on how we wish to react to it being nil
.
Combining Testify with Table-Driven Tests
Incorporating testify
into our test suites doesn’t necessarily preclude us from using methods such as table-driven testing, in fact, it makes it simpler.
package main import ( "testing" "github.com/stretchr/testify/assert" ) func TestCalculate(t *testing.T) { assert := assert.New(t) var tests = []struct { input int expected int }{ {2, 4}, {-1, 1}, {0, 2}, {-5, -3}, {99999, 100001}, } for _, test := range tests { assert.Equal(Calculate(test.input), test.expected) } }
Notice the slight difference between how we called assert.Equal()
in this example compared to the previous example. We’ve initialized assert using assert.New(t)
and we are now able to call assert.Equal()
multiple times, just passing in the input and the expected values as opposed to having to pass t
in as our first parameter every time. This isn’t a big deal, but it certainly helps to make our tests look cleaner.
Mocking
Another excellent feature of the testify
package is it’s mocking capabilities. Mocking effectively allows us to write replacement objects that mock the behaviors of certain objects in our code that we don’t necessarily want to trigger every time we run our test suite.
This could be, for example, a messaging service or an email service that fires off emails to clients whenever it’s called. If we are actively developing our codebase, we might be running our tests hundreds of times per day, and we might not want to send out hundreds of emails and/or messages a day to clients as they may start to take umbrage.
So, how do we go about mocking using the testify
package?
A Mocking Example
Let’s take a look at how we can put mocks
to use with a fairly simple example. In this example, we’ve got a system that will attempt to charge a customer for a product or service. When this ChargeCustomer()
method is called, it will subsequently call a Message Service which will send off an SMS text message to the customer to inform them the amount they have been charged.
package main import ( "fmt" ) // MessageService handles notifying clients they have // been charged type MessageService interface { SendChargeNotification(int) error } // SMSService is our implementation of MessageService type SMSService struct{} // MyService uses the MessageService to notify clients type MyService struct { messageService MessageService } // SendChargeNotification notifies clients they have been // charged via SMS // This is the method we are going to mock func (sms SMSService) SendChargeNotification(value int) error { fmt.Println("Sending Production Charge Notification") return nil } // ChargeCustomer performs the charge to the customer // In a real system we would maybe mock this as well // but here, I want to make some money every time I run my tests func (a MyService) ChargeCustomer(value int) error { a.messageService.SendChargeNotification(value) fmt.Printf("Charging Customer For the value of %d\n", value) return nil } // A "Production" Example func main() { fmt.Println("Hello World") smsService := SMSService{} myService := MyService{smsService} myService.ChargeCustomer(100) }
So, how do we go about testing this to ensure we don’t drive our customers crazy? Well, we mock out our SMSService by creating a new struct
called smsServiceMock
and add mock.Mock to its list of fields.
We then stub out our SendChargeNotification
method so that it doesn’t actually send a notification to our clients and return a nil
error.
Finally, we create our TestChargeCustomer
test function which in turn instantiates a new instance of type smsServiceMock
and specifies what should happen when SendChargeNotification
is called.
package main import ( "fmt" "testing" "github.com/stretchr/testify/mock" ) // smsServiceMock type smsServiceMock struct { mock.Mock } // Our mocked smsService method func (m *smsServiceMock) SendChargeNotification(value int) bool { fmt.Println("Mocked charge notification function") fmt.Printf("Value passed in: %d\n", value) // this records that the method was called and passes in the value // it was called with args := m.Called(value) // it then returns whatever we tell it to return // in this case true to simulate an SMS Service Notification // sent out return args.Bool(0) } // we need to satisfy our MessageService interface // which sadly means we have to stub out every method // defined in that interface func (m *smsServiceMock) DummyFunc() { fmt.Println("Dummy") } // TestChargeCustomer is where the magic happens // here we create our SMSService mock func TestChargeCustomer(t *testing.T) { smsService := new(smsServiceMock) // we then define what should be returned from SendChargeNotification // when we pass in the value 100 to it. In this case, we want to return // true as it was successful in sending a notification smsService.On("SendChargeNotification", 100).Return(true) // next we want to define the service we wish to test myService := MyService{smsService} // and call said method myService.ChargeCustomer(100) // at the end, we verify that our myService.ChargeCustomer // method called our mocked SendChargeNotification method smsService.AssertExpectations(t) }
So, when we run this calling go test ./... -v
we should see the following output:
go test ./... -v === RUN TestChargeCustomer Mocked charge notification function Value passed in: 100 Charging Customer For the value of 100 --- PASS: TestChargeCustomer (0.00s) main_test.go:33: PASS: SendChargeNotification(int) PASS ok _/Users/elliot/Documents/Projects/tutorials/golang/go-testify-tutorial 0.012s
As you can see, our mocked method was called as opposed to our “production” method and we’ve been able to verify that our myService.ChargeCustomer()
method acts the way we expect it to!
Happy days, we’ve now been able to fully test a more complex project using mocks. It’s worth noting that this technique can be used for all manner of different systems, such as mocking database queries or how you interact with other APIs. Overall, mocking is something that is really powerful and is definitely something you should try to master if you are going to be testing production-grade systems in Go.
Generating Mocks with Mockery
So, in the above example we mocked out all of the various methods ourselves, but in real-life examples, this may represent a hell of a lot of different methods and functions to mock.
Thankfully, this is where the vektra/mockery package comes to our aide.
The mockery binary can take in the name of any interfaces
you may have defined within your Go packages and it’ll automatically output the generated mocks to mocks/InterfaceName.go
. This is seriously handy when you want to save yourself a tonne of time and it’s a tool I would highly recommend checking out!
Key Takeaways
- Testify helps you to simplify the way you write assertions within your test cases.
- Testify can also be used to mock objects within your testing framework to ensure you aren’t calling production endpoints whenever you test.
Conclusion
Hopefully, this has helped to demystify the art of testing your Go projects using the stretchr/testify
package. In this tutorial, we’ve managed to look at how you can use assertions from the testify
package to do things like assert if things are equal, or not equal or nil.
We’ve also been able to look at how you can mock out various parts of your systems to ensure that, when running your tests, you don’t subsequently start interacting with production systems and doing things you didn’t quite want to.
If you found this useful, or if you have any comments or feedback, then please feel free to let me know in the comments section below.
Further Reading
If you enjoyed this, you may like my other articles on testing in Go: