29.8 testify: Assertions, Suites, and Mocks
Alright, let’s talk about testify. You’ve probably already felt the raw, existential pain of writing a test with the standard library’s testing package and thought, “There has to be a better way than if got != want { t.Errorf(...) } for the ten thousandth time.” You’re right. There is. Enter testify.
This third-party library is practically part of the standard library at this point, given its ubiquity. It’s a toolkit that gives you three big weapons: assertions to make your test conditions readable, test suites to structure your tests, and mocks to, well, mock things. Let’s break it down.
Assertions: Your New Best Friend
The assert package is the workhorse. It replaces your clunky if conditionals with a single, readable line that gives you fantastic error messages when things go wrong.
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCalculateTax(t *testing.T) {
amount := 100.0
expected := 15.0
// Instead of:
// got := CalculateTax(amount)
// if got != expected {
// t.Errorf("Got %f, expected %f", got, expected)
// }
// You can just do this:
got := CalculateTax(amount)
assert.Equal(t, expected, got, "They should be equal, you fool!")
// And for slices, maps, and other complex types? Magic.
expectedUsers := []string{"Alice", "Bob"}
actualUsers := getUsers()
assert.ElementsMatch(t, expectedUsers, actualUsers) // Order doesn't matter!
}
Why it works: The assert functions use a mix of reflection and very clever error message formatting. assert.Equal(t, expected, actual) uses reflection to deeply compare the two values and, if they differ, prints a beautifully formatted diff showing you exactly what didn’t match. It saves your eyes and your sanity.
Pitfall: Be mindful of the order of arguments. It’s (t, expected, actual). Getting this backwards is a rite of passage, but it will give you confusing error messages. Also, remember these helpers call t.Error for you; they mark the test as failed but continue its execution. If you need to bail immediately, use require.Equal(t, expected, actual) from the require package instead, which calls t.Fatal.
Suites: For When You Need a Little Structure
If your tests are just a series of TestX functions, that’s fine. But if you find yourself repeating the same setup and teardown logic across multiple tests, or you want to group related tests together with shared state, the suite package is your answer. It’s basically object-oriented programming for your tests, and for once, it’s not a terrible idea.
package main
import (
"testing"
"github.com/stretchr/testify/suite"
)
type MyAmazingTestSuite struct {
suite.Suite
databaseConnection *sql.DB
cache map[string]string
}
// This runs once before the entire suite
func (s *MyAmazingTestSuite) SetupSuite() {
conn, err := setupDatabase()
s.Require().NoError(err)
s.databaseConnection = conn
}
// This runs before every single test
func (s *MyAmazingTestSuite) SetupTest() {
s.cache = make(map[string]string) // Fresh cache for every test
}
func (s *MyAmazingTestSuite) TestOne() {
s.cache["key"] = "value"
s.Assert().Len(s.cache, 1) // Use the suite's assertions
}
func (s *MyAmazingTestSuite) TestTwo() {
// This test gets a fresh cache, so it's empty!
s.Assert().Len(s.cache, 0)
}
// This runs after the entire suite
func (s *MyAmazingTestSuite) TearDownSuite() {
s.databaseConnection.Close()
}
// This hook is necessary to run the suite with `go test`
func TestSuite(t *testing.T) {
suite.Run(t, new(MyAmazingTestSuite))
}
Why it works: The suite.Run function uses reflection to find all methods prefixed with Test, and it runs them as individual test cases, wrapping each one with calls to your SetupTest/TearDownTest methods. It’s a fantastic way to keep your test code DRY and well-organized.
Mocks: Controlling the Chaos
This is the most controversial part of testify. The mock package provides a code-generated way to create mock implementations of your interfaces. You use it when a function under test depends on an external service (database, HTTP API, etc.) that you need to simulate and control.
First, you define the interface you want to mock. Then, you use testify’s mock package to generate a mock struct that implements it.
// my_interfaces.go
package main
type DataStore interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
Now, in your test, you can use the mock:
package main
import (
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
func TestGetUserDetails(t *testing.T) {
// Create a mock object
mockStore := new(MockDataStore) // This struct is generated
// Program the mock's expectations
testUser := &User{ID: 42, Name: "Marvin"}
mockStore.On("GetUser", 42).Return(testUser, nil) // When GetUser is called with 42, return this.
// Inject the mock into the code you're testing
result, err := GetUserDetails(mockStore, 42)
// Assert on the result
assert.NoError(t, err)
assert.Equal(t, "Marvin", result.Name)
// Verify the mock was called as expected
mockStore.AssertExpectations(t)
}
Why it works (and the rough edges): The mock records every call made to it. On defines what call to expect and what to return. AssertExpectations(t) at the end is crucial—it fails the test if the expected calls weren’t made. The rough part? You have to generate the MockDataStore code, usually with a //go:generate directive, which adds a build step. Overusing mocks can also lead to tests that are overly coupled to implementation details (i.e., how a function calls its dependencies rather than what it does). Use them for the big, external, non-deterministic things, not for every little internal function call.
So there you have it. testify isn’t perfect, but it’s a massive force multiplier for writing clear, maintainable, and less-infuriating tests. Embrace it. Just use its power wisely.