Testify

unit testingGoGolangtesting frameworkassertionsmockingsuite

Testify

Overview

Testify is a toolkit with common assertions and mocks that plays nicely with the standard library. It extends Go's standard testing package to enable more readable and maintainable test code. While lightweight, it provides powerful features and is widely adopted among Go developers as a testing framework.

Details

Key Features

Simple Assertions

  • Intuitive and readable assertion methods
  • Rich assertions including assert.Equal(), assert.NotNil(), assert.True()
  • Support for custom error messages

Powerful Mocking Capabilities

  • Mock object creation with mock.Mock
  • Setting expectations for method calls
  • Argument matching and response configuration
  • Expectation verification functionality

Test Suite Features

  • Structured testing with suite.Suite
  • Automatic execution of Setup and Teardown
  • Automatic test method discovery and execution

Standard Library Integration

  • Full compatibility with Go's standard testing package
  • Gradual adoption into existing test code possible

Pros and Cons

Pros

  1. Low Learning Curve: Can be used as an extension of Go's standard testing package
  2. Rich Assertions: Dedicated assertions for various data types
  3. Mocking Functionality: Mock external dependencies to isolate unit tests
  4. Lightweight: Fast execution with minimal dependencies
  5. Community Support: Wide adoption in the Go community with abundant examples

Cons

  1. Limited Features: Advanced features are limited compared to other frameworks
  2. Error Messages: Standard error messages can sometimes be insufficient
  3. Parallel Execution: Constraints exist for complex parallel testing scenarios
  4. Mock Complexity: Can become verbose for complex mocking scenarios

Reference Links

Code Examples

Basic Assertion Tests

package main

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

func TestBasicAssertions(t *testing.T) {
    // Test for equality
    assert.Equal(t, 123, 123, "they should be equal")
    
    // Test for inequality
    assert.NotEqual(t, 123, 456, "they should not be equal")
    
    // nil check
    assert.Nil(t, nil)
    
    // not nil check
    var obj interface{} = "something"
    if assert.NotNil(t, obj) {
        // now we know that object isn't nil, we are safe to make
        // further assertions without causing any errors
        assert.Equal(t, "something", obj)
    }
}

func TestCalculator(t *testing.T) {
    calc := NewCalculator()
    
    // Addition test
    result := calc.Add(5, 3)
    assert.Equal(t, 8, result, "5 + 3 should equal 8")
    
    // Subtraction test
    result = calc.Subtract(10, 4)
    assert.Equal(t, 6, result, "10 - 4 should equal 6")
    
    // Division test (error case)
    _, err := calc.Divide(10, 0)
    assert.Error(t, err, "Division by zero should return an error")
    assert.Contains(t, err.Error(), "division by zero")
}

Using Assert Instance

package main

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

func TestWithAssertInstance(t *testing.T) {
    assert := assert.New(t)
    
    // Using assert instance eliminates the need to pass
    // the t parameter for each assertion
    assert.Equal(123, 123, "they should be equal")
    assert.NotEqual(123, 456, "they should not be equal")
    assert.Nil(nil)
    
    var obj interface{} = "something"
    if assert.NotNil(obj) {
        assert.Equal("something", obj)
    }
}

func TestStringOperations(t *testing.T) {
    assert := assert.New(t)
    
    text := "Hello, World!"
    
    assert.Contains(text, "World")
    assert.NotContains(text, "xyz")
    assert.Len(text, 13)
    assert.True(len(text) > 10)
}

Using Mock Functionality

package main

import (
    "testing"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/assert"
)

// Interface under test
type DataService interface {
    GetData(id int) (string, error)
    SaveData(id int, data string) error
}

// Mock object
type MockDataService struct {
    mock.Mock
}

func (m *MockDataService) GetData(id int) (string, error) {
    args := m.Called(id)
    return args.String(0), args.Error(1)
}

func (m *MockDataService) SaveData(id int, data string) error {
    args := m.Called(id, data)
    return args.Error(0)
}

// Business logic under test
type UserService struct {
    dataService DataService
}

func (s *UserService) ProcessUser(userID int) (string, error) {
    data, err := s.dataService.GetData(userID)
    if err != nil {
        return "", err
    }
    
    processedData := "Processed: " + data
    err = s.dataService.SaveData(userID, processedData)
    if err != nil {
        return "", err
    }
    
    return processedData, nil
}

func TestUserService(t *testing.T) {
    // Create mock object
    mockService := new(MockDataService)
    
    // Set up expectations
    mockService.On("GetData", 123).Return("user data", nil)
    mockService.On("SaveData", 123, "Processed: user data").Return(nil)
    
    // Execute code under test
    userService := &UserService{dataService: mockService}
    result, err := userService.ProcessUser(123)
    
    // Assertions
    assert.NoError(t, err)
    assert.Equal(t, "Processed: user data", result)
    
    // Verify expectations were met
    mockService.AssertExpectations(t)
}

func TestUserServiceWithError(t *testing.T) {
    mockService := new(MockDataService)
    
    // Set up expectations for error case
    mockService.On("GetData", 456).Return("", assert.AnError)
    
    userService := &UserService{dataService: mockService}
    result, err := userService.ProcessUser(456)
    
    // Error case assertions
    assert.Error(t, err)
    assert.Empty(t, result)
    
    mockService.AssertExpectations(t)
}

Using Placeholders in Mocks

func TestWithPlaceholders(t *testing.T) {
    mockService := new(MockDataService)
    
    // Use mock.Anything to accept any argument
    mockService.On("GetData", mock.Anything).Return("default data", nil)
    mockService.On("SaveData", mock.AnythingOfType("int"), mock.AnythingOfType("string")).Return(nil)
    
    userService := &UserService{dataService: mockService}
    
    // Test with multiple different IDs
    result1, err1 := userService.ProcessUser(100)
    result2, err2 := userService.ProcessUser(200)
    
    assert.NoError(t, err1)
    assert.NoError(t, err2)
    assert.Equal(t, "Processed: default data", result1)
    assert.Equal(t, "Processed: default data", result2)
    
    mockService.AssertExpectations(t)
}

func TestMockWithCallbacks(t *testing.T) {
    mockService := new(MockDataService)
    
    // Dynamic response using callback function
    mockService.On("GetData", mock.AnythingOfType("int")).Return("", nil).Run(func(args mock.Arguments) {
        id := args.Get(0).(int)
        // Execute some processing based on ID
        assert.True(t, id > 0, "ID should be positive")
    })
    
    userService := &UserService{dataService: mockService}
    _, err := userService.ProcessUser(123)
    
    assert.NoError(t, err)
    mockService.AssertExpectations(t)
}

Using Test Suites

package main

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

// Test suite definition
type CalculatorTestSuite struct {
    suite.Suite
    calculator *Calculator
}

// Test suite initialization (executed before each test)
func (suite *CalculatorTestSuite) SetupTest() {
    suite.calculator = NewCalculator()
}

// Test methods (names starting with "Test")
func (suite *CalculatorTestSuite) TestAddition() {
    result := suite.calculator.Add(5, 3)
    suite.Equal(8, result, "5 + 3 should equal 8")
}

func (suite *CalculatorTestSuite) TestSubtraction() {
    result := suite.calculator.Subtract(10, 4)
    suite.Equal(6, result, "10 - 4 should equal 6")
}

func (suite *CalculatorTestSuite) TestDivision() {
    result, err := suite.calculator.Divide(15, 3)
    suite.NoError(err, "Valid division should not return error")
    suite.Equal(5, result, "15 / 3 should equal 5")
}

func (suite *CalculatorTestSuite) TestDivisionByZero() {
    _, err := suite.calculator.Divide(10, 0)
    suite.Error(err, "Division by zero should return error")
    suite.Contains(err.Error(), "division by zero")
}

// Test function to run the suite
func TestCalculatorTestSuite(t *testing.T) {
    suite.Run(t, new(CalculatorTestSuite))
}

Suite with Built-in Assertions

type DatabaseTestSuite struct {
    suite.Suite
    db     *sql.DB
    userRepo *UserRepository
}

func (suite *DatabaseTestSuite) SetupSuite() {
    // Suite-wide initialization (executed once)
    var err error
    suite.db, err = sql.Open("sqlite3", ":memory:")
    suite.Require().NoError(err, "Failed to connect to database")
    
    // Create table
    _, err = suite.db.Exec(`
        CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL
        )
    `)
    suite.Require().NoError(err, "Failed to create table")
}

func (suite *DatabaseTestSuite) SetupTest() {
    // Executed before each test
    suite.userRepo = NewUserRepository(suite.db)
    
    // Clean up test data
    _, err := suite.db.Exec("DELETE FROM users")
    suite.Require().NoError(err)
}

func (suite *DatabaseTestSuite) TestCreateUser() {
    user := &User{
        Name:  "John Doe",
        Email: "[email protected]",
    }
    
    err := suite.userRepo.Create(user)
    suite.NoError(err)
    suite.NotEqual(0, user.ID, "User ID should be set")
}

func (suite *DatabaseTestSuite) TestGetUser() {
    // Insert test data
    result, err := suite.db.Exec(
        "INSERT INTO users (name, email) VALUES (?, ?)",
        "Jane Doe", "[email protected]",
    )
    suite.Require().NoError(err)
    
    id, err := result.LastInsertId()
    suite.Require().NoError(err)
    
    // Get user
    user, err := suite.userRepo.GetByID(int(id))
    suite.NoError(err)
    suite.NotNil(user)
    suite.Equal("Jane Doe", user.Name)
    suite.Equal("[email protected]", user.Email)
}

func (suite *DatabaseTestSuite) TearDownSuite() {
    // Suite-wide cleanup (executed once)
    if suite.db != nil {
        suite.db.Close()
    }
}

func TestDatabaseTestSuite(t *testing.T) {
    suite.Run(t, new(DatabaseTestSuite))
}

Advanced Assertion Examples

func TestAdvancedAssertions(t *testing.T) {
    assert := assert.New(t)
    
    // Slice tests
    numbers := []int{1, 2, 3, 4, 5}
    assert.Len(numbers, 5)
    assert.Contains(numbers, 3)
    assert.NotContains(numbers, 10)
    assert.ElementsMatch([]int{5, 4, 3, 2, 1}, numbers) // Order-independent comparison
    
    // Map tests
    userMap := map[string]int{
        "alice": 25,
        "bob":   30,
        "carol": 28,
    }
    assert.Len(userMap, 3)
    assert.Equal(25, userMap["alice"])
    assert.NotContains(userMap, "david")
    
    // Struct tests
    user1 := User{ID: 1, Name: "Alice", Email: "[email protected]"}
    user2 := User{ID: 1, Name: "Alice", Email: "[email protected]"}
    user3 := User{ID: 2, Name: "Bob", Email: "[email protected]"}
    
    assert.Equal(user1, user2)
    assert.NotEqual(user1, user3)
    
    // JSON comparison
    jsonString := `{"id":1,"name":"Alice","email":"[email protected]"}`
    assert.JSONEq(jsonString, `{"name":"Alice","id":1,"email":"[email protected]"}`)
    
    // Regular expression tests
    email := "[email protected]"
    assert.Regexp(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
}