testify Golang Guide: Assertions, Mocks, and Test Suites
testify is the most popular Go testing library. It provides three main packages: assert for fluent assertions, require for fail-fast assertions, mock for interface mocking, and suite for test organization. While the standard testing package is sufficient for simple cases, testify makes complex test suites significantly more readable.
Installation
go get github.com/stretchr/testifyImport the packages you need:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)assert Package
assert provides fluent assertion functions. On failure, the test is marked as failed but continues running:
func TestUser(t *testing.T) {
user := NewUser("Alice", "alice@example.com")
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, "alice@example.com", user.Email)
assert.True(t, user.IsActive)
assert.NotNil(t, user.CreatedAt)
}Common Assertions
Equality:
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
assert.EqualValues(t, expected, actual) // converts types for comparisonNil and zero:
assert.Nil(t, err)
assert.NotNil(t, user)
assert.Zero(t, count)
assert.NotZero(t, id)Boolean:
assert.True(t, isValid)
assert.False(t, hasError)Strings:
assert.Contains(t, "hello world", "world")
assert.NotContains(t, output, "error")
assert.HasPrefix(t, url, "https://")
assert.HasSuffix(t, filename, ".go")
assert.Regexp(t, `^\d{3}-\d{4}$`, phone)Collections:
assert.Len(t, items, 5)
assert.Empty(t, list)
assert.NotEmpty(t, results)
assert.Contains(t, []int{1, 2, 3}, 2)
assert.ElementsMatch(t, expected, actual) // order-independentTypes:
assert.IsType(t, User{}, result)
assert.Implements(t, (*io.Reader)(nil), obj)Errors:
assert.Error(t, err)
assert.NoError(t, err)
assert.EqualError(t, err, "expected message")
assert.ErrorIs(t, err, ErrNotFound)
assert.ErrorAs(t, err, &target)Custom Messages
Add context to failures with a format string:
assert.Equal(t, 200, resp.StatusCode, "expected OK response, got: %s", resp.Body)require Package
require is identical to assert but calls t.FailNow() on failure — the test stops immediately:
func TestCreateOrder(t *testing.T) {
user, err := CreateUser("Alice")
require.NoError(t, err) // stops if err != nil
require.NotNil(t, user) // stops if user == nil
order, err := CreateOrder(user.ID)
require.NoError(t, err)
assert.Equal(t, user.ID, order.UserID) // continues on failure
}Rule of thumb:
- Use
requirewhen the test cannot proceed if the assertion fails (setup steps, preconditions) - Use
assertfor the actual test assertions where you want to collect all failures
mock Package
testify/mock lets you mock interfaces for unit testing:
// Interface to mock
type EmailSender interface {
Send(to, subject, body string) error
}
// Auto-generate or write the mock
type MockEmailSender struct {
mock.Mock
}
func (m *MockEmailSender) Send(to, subject, body string) error {
args := m.Called(to, subject, body)
return args.Error(0)
}Setting Up Expectations
func TestWelcomeEmail(t *testing.T) {
sender := new(MockEmailSender)
// Expect Send to be called with specific arguments
sender.On("Send", "alice@example.com", "Welcome!", mock.AnythingOfType("string")).
Return(nil)
service := UserService{Email: sender}
err := service.RegisterUser("alice@example.com")
assert.NoError(t, err)
sender.AssertExpectations(t) // verifies all expected calls happened
}Argument Matchers
// Exact value
mock.On("Send", "alice@example.com", "Subject", "Body")
// Any value
mock.On("Send", mock.Anything, "Subject", mock.Anything)
// Type matcher
mock.On("Send", mock.AnythingOfType("string"), ...)
// Custom matcher
mock.On("Send", mock.MatchedBy(func(email string) bool {
return strings.Contains(email, "@")
}), ...)Return Values
// Return static value
mock.On("FindUser", 1).Return(&User{Name: "Alice"}, nil)
// Return error
mock.On("FindUser", 99).Return(nil, ErrNotFound)
// Return function result
mock.On("FindUser", mock.Anything).Return(func(id int) *User {
return &User{ID: id}
}, nil)Call Counts
// Called exactly once
sender.On("Send", ...).Return(nil).Once()
// Called exactly N times
sender.On("Send", ...).Return(nil).Times(3)
// Called at least once (default behavior with AssertExpectations)
sender.AssertCalled(t, "Send", "alice@example.com", mock.Anything, mock.Anything)
sender.AssertNotCalled(t, "Send", "bob@example.com", mock.Anything, mock.Anything)
sender.AssertNumberOfCalls(t, "Send", 2)Generating Mocks with mockery
Writing mock structs by hand is tedious. Use mockery to auto-generate them:
go install github.com/vektra/mockery/v2@latest
mockery --name EmailSender --output ./mocksThis generates a complete mock struct in mocks/EmailSender.go. Run it after every interface change.
suite Package
testify/suite provides test suites — structs that group related tests and support lifecycle hooks:
type UserServiceSuite struct {
suite.Suite
db *sql.DB
service *UserService
}
func (s *UserServiceSuite) SetupSuite() {
// Runs once before all tests in the suite
s.db = connectTestDB()
}
func (s *UserServiceSuite) TearDownSuite() {
// Runs once after all tests in the suite
s.db.Close()
}
func (s *UserServiceSuite) SetupTest() {
// Runs before each test
s.service = NewUserService(s.db)
s.db.Exec("DELETE FROM users")
}
func (s *UserServiceSuite) TearDownTest() {
// Runs after each test
}
func (s *UserServiceSuite) TestCreateUser_Valid() {
user, err := s.service.CreateUser("Alice", "alice@example.com")
s.Require().NoError(err)
s.Assert().Equal("Alice", user.Name)
}
func (s *UserServiceSuite) TestCreateUser_DuplicateEmail() {
s.service.CreateUser("Alice", "alice@example.com")
_, err := s.service.CreateUser("Bob", "alice@example.com")
s.Assert().ErrorIs(err, ErrDuplicateEmail)
}
// Register the suite with go test
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceSuite))
}s.Assert() and s.Require() are the suite's embedded assertion methods. You can also call s.Equal(...), s.NoError(...) directly — the suite embeds assert.Assertions.
When to Use Suites
Suites are appropriate when:
- Tests share expensive setup (database, HTTP server)
- You want clear lifecycle hooks
- Tests are logically grouped and need shared state
For simple unit tests, plain t.Run subtests are cleaner.
Combining Patterns
A typical Go test file using testify:
package service_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/mock"
)
func TestOrderService_PlaceOrder(t *testing.T) {
// Arrange
inventory := new(MockInventoryService)
payment := new(MockPaymentGateway)
inventory.On("Reserve", "SKU-123", 1).Return(nil)
payment.On("Charge", mock.AnythingOfType("float64")).Return("txn_123", nil)
svc := NewOrderService(inventory, payment)
// Act
order, err := svc.PlaceOrder("user-1", "SKU-123", 1, 29.99)
// Assert
require.NoError(t, err)
require.NotNil(t, order)
assert.Equal(t, "user-1", order.UserID)
assert.Equal(t, "txn_123", order.TransactionID)
inventory.AssertExpectations(t)
payment.AssertExpectations(t)
}testify vs Standard Library
| Feature | Standard library | testify |
|---|---|---|
| Assertions | Manual if comparisons |
Fluent assertion functions |
| Failure messages | Manual t.Errorf |
Auto-generated with values |
| Mocking | Manual stubs | mock package with expectations |
| Test organization | t.Run subtests |
Suites with lifecycle hooks |
| Dependencies | Zero | One module |
Use testify when you want cleaner test code and find yourself writing the same assertion boilerplate repeatedly. Skip it for simple packages where the standard library is sufficient.