testify Suite and Mocking in Go: Complete Guide

testify Suite and Mocking in Go: Complete Guide

testify/assert continues tests on failure; testify/require stops immediately. testify/suite groups tests with SetupTest/TearDownTest lifecycle hooks. testify/mock generates verifiable mock objects — call mock.AssertExpectations(t) at the end of every test. Combine suite + mock for organized integration-style tests.

testify is the most widely used Go testing library. It does not replace the standard testing package — it extends it with better assertion messages, test lifecycle management, and a mock framework that integrates cleanly with Go interfaces. Understanding when to use assert versus require, how suite manages state, and how mock enforces expectations will raise the quality of your entire test suite.

testify/assert and require: The Critical Difference

Both packages provide the same assertion functions. The difference is what happens after a failure.

assert records the failure and continues executing the test:

func TestUserValidation(t *testing.T) {
    u := User{Name: "", Email: "not-an-email", Age: -1}

    assert.NotEmpty(t, u.Name, "name should not be empty")       // fails, continues
    assert.Contains(t, u.Email, "@", "email must contain @")     // fails, continues
    assert.Greater(t, u.Age, 0, "age must be positive")          // fails, continues
    // all three failures reported in one test run
}

require calls t.FailNow() on failure, stopping the test immediately:

func TestParseConfig(t *testing.T) {
    cfg, err := ParseConfig("config.yaml")
    require.NoError(t, err)       // if this fails, stop — nothing below makes sense
    require.NotNil(t, cfg)        // same logic

    assert.Equal(t, "localhost", cfg.Host)   // now safe to check fields
    assert.Equal(t, 8080, cfg.Port)
}

Use require for preconditions — nil pointer checks, error checks where subsequent code would panic or produce misleading failures. Use assert for the actual assertions where you want to see all failures at once.

Common Assertion Patterns

// Equality
assert.Equal(t, expected, actual)
assert.NotEqual(t, unexpected, actual)

// Nil checks
assert.Nil(t, err)
assert.NotNil(t, obj)

// Boolean
assert.True(t, condition, "message")
assert.False(t, condition, "message")

// Collections
assert.Len(t, slice, 3)
assert.Contains(t, slice, item)
assert.ElementsMatch(t, expected, actual) // order-independent slice equality

// Errors
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrNotFound)
assert.ErrorAs(t, err, &target)

// Strings
assert.Contains(t, str, substring)
assert.HasPrefix(t, str, "prefix")
assert.Regexp(t, `^\d{4}-\d{2}-\d{2}$`, dateStr)

// Numeric
assert.Greater(t, a, b)
assert.GreaterOrEqual(t, a, b)
assert.InDelta(t, 3.14, actual, 0.001) // for floats

Every assertion accepts an optional final msgAndArgs variadic — a format string and arguments printed on failure:

assert.Equal(t, want, got, "user %d: field %s mismatch", userID, fieldName)

testify/suite: Setup and Teardown

testify/suite provides xUnit-style lifecycle hooks. A suite is a struct that embeds suite.Suite:

import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type UserServiceSuite struct {
    suite.Suite
    db      *sql.DB
    service *UserService
}

// Runs once before all tests in the suite
func (s *UserServiceSuite) SetupSuite() {
    db, err := sql.Open("postgres", testDSN)
    s.Require().NoError(err)
    s.db = db
    runMigrations(s.db)
}

// Runs before each test method
func (s *UserServiceSuite) SetupTest() {
    s.service = NewUserService(s.db)
    seedTestData(s.db)
}

// Runs after each test method
func (s *UserServiceSuite) TearDownTest() {
    truncateAllTables(s.db)
}

// Runs once after all tests in the suite
func (s *UserServiceSuite) TearDownSuite() {
    s.db.Close()
}

// Test methods must start with "Test"
func (s *UserServiceSuite) TestCreateUser() {
    user, err := s.service.Create(CreateUserInput{Name: "alice", Email: "alice@example.com"})
    s.Require().NoError(err)
    s.Assert().NotZero(user.ID)
    s.Assert().Equal("alice", user.Name)
}

func (s *UserServiceSuite) TestGetUser_NotFound() {
    _, err := s.service.Get(99999)
    s.Assert().ErrorIs(err, ErrUserNotFound)
}

// Entry point — one TestXxx function per suite
func TestUserService(t *testing.T) {
    suite.Run(t, new(UserServiceSuite))
}

Inside suite methods, use s.Require() and s.Assert() rather than the package-level functions — they are already bound to the correct *testing.T for the current method.

The SetupTest / TearDownTest pair runs around every test method. This is the key advantage over a flat TestMain — each test starts with a clean state without manual cleanup in each test.

testify/mock: Dependency Mocking

testify/mock generates mock objects that implement your interfaces and record calls for assertion. Define the mock by embedding mock.Mock and implementing the interface:

// The interface under test
type EmailSender interface {
    Send(to, subject, body string) error
}

// The mock implementation
type MockEmailSender struct {
    mock.Mock
}

func (m *MockEmailSender) Send(to, subject, body string) error {
    args := m.Called(to, subject, body)
    return args.Error(0)
}

m.Called(...) records the call and returns the configured return values. Now use it in a test:

func TestUserRegistration_SendsWelcomeEmail(t *testing.T) {
    mockEmail := new(MockEmailSender)

    // Set expectation: Send must be called exactly once with these args
    mockEmail.On("Send",
        "alice@example.com",
        "Welcome to HelpMeTest",
        mock.AnythingOfType("string"), // body can be anything
    ).Return(nil)

    svc := NewUserService(mockEmail)
    _, err := svc.Register("alice@example.com", "alice")
    assert.NoError(t, err)

    // Verify all expectations were met
    mockEmail.AssertExpectations(t)
}

mock.AssertExpectations(t) fails the test if any expected call was not made, or if unexpected calls were made. Always call it at the end of every test that uses a mock.

Mock Matchers

testify/mock provides several argument matchers:

mock.Anything                           // matches any value
mock.AnythingOfType("string")           // matches any string
mock.AnythingOfType("*models.User")     // matches any *models.User pointer
mock.MatchedBy(func(u User) bool {      // custom matcher
    return u.Email != ""
})

Use MatchedBy when you need to assert on a subset of struct fields:

mockDB.On("Save", mock.MatchedBy(func(u User) bool {
    return u.Name == "alice" && u.CreatedAt.After(time.Now().Add(-time.Minute))
})).Return(nil)

Controlling Return Values and Side Effects

// Return different values on successive calls
mockEmail.On("Send", mock.Anything, mock.Anything, mock.Anything).
    Return(nil).Once()          // first call succeeds
mockEmail.On("Send", mock.Anything, mock.Anything, mock.Anything).
    Return(ErrSMTPTimeout).Once() // second call fails

// Run a function on call (useful for capturing arguments)
var capturedBody string
mockEmail.On("Send", mock.Anything, mock.Anything, mock.AnythingOfType("string")).
    Run(func(args mock.Arguments) {
        capturedBody = args.String(2)
    }).
    Return(nil)

After the test, inspect capturedBody to assert on values computed dynamically inside the service.

Combining Suite with Mock

The most powerful pattern combines suite.Suite for lifecycle management with mock for dependency isolation:

type OrderServiceSuite struct {
    suite.Suite
    mockPayment *MockPaymentGateway
    mockEmail   *MockEmailSender
    service     *OrderService
}

func (s *OrderServiceSuite) SetupTest() {
    s.mockPayment = new(MockPaymentGateway)
    s.mockEmail = new(MockEmailSender)
    s.service = NewOrderService(s.mockPayment, s.mockEmail)
}

func (s *OrderServiceSuite) TearDownTest() {
    // Verify all mocks after each test
    s.mockPayment.AssertExpectations(s.T())
    s.mockEmail.AssertExpectations(s.T())
}

func (s *OrderServiceSuite) TestPlaceOrder_Success() {
    s.mockPayment.On("Charge", "card_123", 9999).Return("txn_abc", nil)
    s.mockEmail.On("Send", "user@example.com", "Order confirmed", mock.Anything).Return(nil)

    order, err := s.service.Place(PlaceOrderInput{
        CardToken: "card_123",
        AmountCents: 9999,
        UserEmail: "user@example.com",
    })
    s.Require().NoError(err)
    s.Assert().Equal("txn_abc", order.TransactionID)
    s.Assert().Equal(OrderStatusConfirmed, order.Status)
}

func (s *OrderServiceSuite) TestPlaceOrder_PaymentDeclined() {
    s.mockPayment.On("Charge", "card_bad", 9999).Return("", ErrCardDeclined)
    // Email should NOT be called when payment fails
    // (no mockEmail.On — any call will panic the test)

    _, err := s.service.Place(PlaceOrderInput{
        CardToken: "card_bad",
        AmountCents: 9999,
        UserEmail: "user@example.com",
    })
    s.Assert().ErrorIs(err, ErrCardDeclined)
}

func TestOrderService(t *testing.T) {
    suite.Run(t, new(OrderServiceSuite))
}

Moving AssertExpectations to TearDownTest means you cannot forget it — it runs automatically after every test method. Fresh mocks in SetupTest ensure no state leaks between tests.

When Not to Mock

Mocking should be applied at architectural boundaries — external services, databases, file systems, clocks — not at every function call. If you find yourself mocking internal helpers, the design likely needs a refactor. Mocks that are tightly coupled to implementation details (asserting a specific internal function is called three times) make tests brittle and refactoring painful.

Prefer real implementations for fast, deterministic dependencies. Use an in-memory SQLite database or a testcontainers-managed Postgres over mocking the entire *sql.DB interface. Reserve mocks for things you genuinely cannot or should not instantiate in tests: payment processors, SMS gateways, external APIs with rate limits.

Key Takeaways

Use require for preconditions and assert for assertions — both in the same test. Use suite.Suite when multiple tests share expensive setup or need per-test cleanup. Use testify/mock at architectural boundaries, always call AssertExpectations(t), and move it to TearDownTest when using suites. The combination of suite lifecycle management and mock verification produces tests that are isolated, readable, and impossible to accidentally leave in a partially-set-up state.

Read more

Start now free