BrandishBot_Go uses mockery to auto-generate type-safe mocks for testing. This replaces manual mock implementations with professional, maintainable generated code.
import (
"github.com/osse101/BrandishBot_Go/mocks"
usermocks "github.com/osse101/BrandishBot_Go/internal/user/mocks"
)
func TestHandleAddItem(t *testing.T) {
// Create service mock (from global mocks)
mockSvc := mocks.NewMockUserService(t)
// Create repository mock (from local package mocks)
mockRepo := usermocks.NewMockRepository(t)
// Set expectations
mockSvc.On("AddItem", mock.Anything, "twitch", "id", "user", "Sword", 1).
Return(nil)
// Use in test
handler := HandleAddItem(mockSvc)
// ... test code
// Verify expectations met
mockSvc.AssertExpectations(t)
}When interfaces change:
make mocksMocks are distributed to avoid import cycles:
Global Mocks (mocks/ package):
- Service Mocks:
MockUserService,MockEconomyService,MockProgressionService - Core Interfaces:
MockEventBus,MockDBPool - Used primarily for testing Handlers and Controllers.
Local Mocks (internal/<package>/mocks/):
- Repository Mocks:
MockRepository(within each domain package) - Tx Mocks:
MockTx(transaction wrappers) - Used for testing Services without creating circular dependencies.
See .mockery.yaml for complete configuration.
Best for: Simple dependency mocking
func TestHandler(t *testing.T) {
// ✅ Use mockery for clean, type-safe handler tests
mockSvc := mocks.NewMockUserService(t)
mockBus := mocks.NewMockEventBus(t)
mockSvc.On("GetUser", "123").Return(user, nil)
mockBus.On("Publish", mock.Anything, mock.Anything).Return(nil)
handler := NewHandler(mockSvc, mockBus)
// ... test
}Best for: Complex state management
// ✅ Keep functional in-memory mocks for service tests
type MockRepository struct {
users map[string]*domain.User
}
func (m *MockRepository) GetUser(id string) (*domain.User, error) {
if user, ok := m.users[id]; ok {
return user, nil
}
return nil, ErrNotFound
}| Test Type | Use Mockery | Use Functional Mock |
|---|---|---|
| Handler/Controller | ✅ Yes | ❌ No |
| Service/Business Logic | Maybe | ✅ Preferred |
| Repository/Data Layer | ✅ Yes | Only if complex |
| Simple Utilities | ❌ No mocks needed | ❌ No mocks needed |
// Simple return value
mock.On("MethodName", arg1, arg2).Return(returnValue, nil)
// Multiple calls
mock.On("GetUser", "123").Return(user1, nil).Once()
mock.On("GetUser", "456").Return(user2, nil).Once()
// Any argument
mock.On("AddItem", mock.Anything, mock.Anything).Return(nil)
// Specific + any
mock.On("AddItem", mock.Anything, "platform", mock.Anything).Return(nil)// Custom matcher
mock.On("Publish", mock.Anything, mock.MatchedBy(func(evt event.Event) bool {
return evt.Type == "item.sold"
})).Return(nil)
// Type matcher
mock.On("UpdateUser", mock.AnythingOfType("*domain.User")).Return(nil)// Error cases
mock.On("GetUser", "invalid").Return(nil, ErrNotFound)
// Panic
mock.On("DangerousMethod").Panic("Something went wrong")
// Run custom function
mock.On("ProcessItem", mock.Anything).Run(func(args mock.Arguments) {
item := args.Get(0).(*domain.Item)
item.Processed = true
}).Return(nil)func TestHandler_ErrorHandling(t *testing.T) {
mockSvc := mocks.NewMockUserService(t)
// Setup error expectation
mockSvc.On("GetUser", "missing").
Return(nil, user.ErrNotFound)
handler := NewHandler(mockSvc)
result, err := handler.GetUser("missing")
assert.Error(t, err)
assert.ErrorIs(t, err, user.ErrNotFound)
}func TestHandler_PublishesEvent(t *testing.T) {
mockBus := mocks.NewMockEventBus(t)
// Expect specific event
mockBus.On("Publish", mock.Anything, mock.MatchedBy(func(evt event.Event) bool {
return evt.Type == "user.created" && evt.UserID == "123"
})).Return(nil)
// ... test that triggers event
mockBus.AssertExpectations(t)
}func TestUserService(t *testing.T) {
tests := []struct {
name string
userID string
setupMock func(*mocks.MockUserRepository)
wantErr bool
}{
{
name: "user exists",
userID: "123",
setupMock: func(m *mocks.MockUserRepository) {
m.On("GetUserByID", mock.Anything, "123").
Return(&domain.User{ID: "123"}, nil)
},
wantErr: false,
},
{
name: "user not found",
userID: "missing",
setupMock: func(m *mocks.MockUserRepository) {
m.On("GetUserByID", mock.Anything, "missing").
Return(nil, user.ErrNotFound)
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := mocks.NewMockUserRepository(t)
tt.setupMock(mockRepo)
svc := user.NewService(mockRepo)
_, err := svc.GetUser(context.Background(), tt.userID)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}Mocks are configured in .mockery.yaml using a mixed strategy:
packages:
github.com/osse101/BrandishBot_Go/internal/user:
config:
# Service mocks go to global mocks/ folder
filename: 'mock_user_{{.InterfaceName | snakecase}}.go'
mockname: 'MockUser{{.InterfaceName}}'
interfaces:
Service:
Repository:
config:
# Repository mocks go to internal/user/mocks/ folder
dir: '{{.InterfaceDir}}/mocks'
outpkg: 'mocks'
filename: 'mock_repository.go'
mockname: 'MockRepository'
with-expecter: trueKey settings:
- Service Mocks: Generated in root
mocks/for ease of use in handlers. - Repository Mocks: Generated in
internal/<pkg>/mocks/to prevent import cycles. with-expecter: true- Enables type-safe EXPECT() pattern.
- Add interface to
.mockery.yaml:
packages:
github.com/osse101/BrandishBot_Go/internal/mypackage:
config:
filename: 'mock_mypackage_{{.InterfaceName | snakecase}}.go'
mockname: 'MockMypackage{{.InterfaceName}}'
interfaces:
MyNewInterface:- Regenerate:
make mocks- Use in tests:
mock := mocks.NewMockMypackageMyNewInterface(t)Error: undefined: mocks.NewMockXXX
Solution: Interface not in .mockery.yaml or mocks not generated
make mocks # Regenerate all mocksError: mock.EXPECT().MethodName undefined
Solution: Interface changed but mocks not updated
make mocks # Regenerate after interface changesError: FAIL: 0 out of 1 expectation(s) were met
Solution: Mock expected a call that didn't happen or arguments didn't match
// Debug by checking exact arguments
mockSvc.On("Method", mock.Anything).Return(nil) // Less specific
// vs
mockSvc.On("Method", "exact", "args").Return(nil) // More specific✅ DO:
- Use mockery for handler/controller tests
- Regenerate mocks when interfaces change
- Use
mock.Anythingfor irrelevant arguments - Verify expectations with
AssertExpectations(t)
❌ DON'T:
- Mock everything - keep functional mocks for complex state
- Forget to call
AssertExpectations(t) - Over-specify expectations (brittle tests)
- Create manual mocks for new code
Recommended structure for packages with mocks:
internal/<package>/
├── repository.go # Interface definition (or wrapper)
├── fake_repository.go # Optional: Stateful fake (manual)
├── mocks/
│ └── mock_repository.go # Generated by mockery
└── *_test.go # Tests using either mock type
While Mockery handles most cases, stateful fakes are better for integration-style tests where you need to verify complex state transitions without setting up dozens of expectations.
Example: Integration Test with State Manipulation
package user_test
import (
"testing"
"github.com/osse101/BrandishBot_Go/internal/user"
)
func TestService_ComplexWorkflow(t *testing.T) {
// 1. Create fake with initial state (not a generated mock)
fake := user.NewFakeRepository()
fake.Users["alice"] = &domain.User{
Username: "alice",
Money: 1000,
}
// 2. Create service with fake
svc := user.NewService(fake)
// 3. Run complex workflow
err := svc.BuyItem(context.Background(), "alice", "sword")
require.NoError(t, err)
// 4. Verify state changes directly
user := fake.Users["alice"]
assert.Equal(t, 900, user.Money) // Spent 100
assert.Contains(t, fake.Inventories["alice"].Slots, "sword")
}- Mockery Documentation
- Testify Mock Package
- Project Configuration
- Handler Test Examples