testify Golang Guide: Assertions, Mocks, and Test Suites

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/testify

Import 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 comparison

Nil 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-independent

Types:

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 require when the test cannot proceed if the assertion fails (setup steps, preconditions)
  • Use assert for 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 ./mocks

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

Read more