Testify
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
testingpackage - Gradual adoption into existing test code possible
Pros and Cons
Pros
- Low Learning Curve: Can be used as an extension of Go's standard testing package
- Rich Assertions: Dedicated assertions for various data types
- Mocking Functionality: Mock external dependencies to isolate unit tests
- Lightweight: Fast execution with minimal dependencies
- Community Support: Wide adoption in the Go community with abundant examples
Cons
- Limited Features: Advanced features are limited compared to other frameworks
- Error Messages: Standard error messages can sometimes be insufficient
- Parallel Execution: Constraints exist for complex parallel testing scenarios
- Mock Complexity: Can become verbose for complex mocking scenarios
Reference Links
- GitHub: stretchr/testify
- Go Doc: pkg.go.dev/github.com/stretchr/testify
- Assert Package: pkg.go.dev/github.com/stretchr/testify/assert
- Mock Package: pkg.go.dev/github.com/stretchr/testify/mock
- Suite Package: pkg.go.dev/github.com/stretchr/testify/suite
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)
}