Testing Guide
Testing with godi is simple and powerful. Replace real services with mocks instantly, run tests in milliseconds, and achieve better coverage with less effort.
The Basic Pattern
func TestUserService(t *testing.T) {
// 1. Create test module with mocks
testModule := godi.NewModule("test",
godi.AddSingleton(func() *Database {
return &MockDatabase{
users: []User{{ID: "1", Name: "Alice"}},
}
}),
godi.AddSingleton(func() *Logger {
return &MockLogger{}
}),
godi.AddScoped(NewUserService), // Real service, mock dependencies
)
// 2. Build provider
collection := godi.NewCollection()
collection.AddModules(testModule)
provider, _ := collection.Build()
defer provider.Close()
// 3. Test!
service, _ := godi.Resolve[*UserService](provider)
user, err := service.GetUser("1")
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
Creating Mocks
Simple Mock
type MockDatabase struct {
users map[string]*User
}
func (m *MockDatabase) GetUser(id string) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, errors.New("not found")
}
return user, nil
}
func (m *MockDatabase) SaveUser(user *User) error {
m.users[user.ID] = user
return nil
}
Mock with Behavior Control
type MockEmailService struct {
shouldFail bool
sentEmails []Email
}
func (m *MockEmailService) Send(to, subject, body string) error {
if m.shouldFail {
return errors.New("email service down")
}
m.sentEmails = append(m.sentEmails, Email{
To: to,
Subject: subject,
Body: body,
})
return nil
}
Spy Pattern for Verification
type SpyLogger struct {
messages []string
callCount int
}
func (s *SpyLogger) Log(msg string) {
s.callCount++
s.messages = append(s.messages, msg)
}
// In your test
func TestLogging(t *testing.T) {
spy := &SpyLogger{}
testModule := godi.NewModule("test",
godi.AddSingleton(func() Logger {
return spy
}),
godi.AddScoped(NewUserService),
)
// ... run test ...
// Verify behavior
assert.Equal(t, 2, spy.callCount)
assert.Contains(t, spy.messages, "User created")
}
Test Helpers
Create reusable test utilities:
// testutil/helpers.go
package testutil
import (
"testing"
"github.com/junioryono/godi/v4"
"github.com/stretchr/testify/require"
)
// BuildTestProvider creates a provider and ensures cleanup
func BuildTestProvider(t *testing.T, modules ...godi.ModuleOption) godi.Provider {
collection := godi.NewCollection()
err := collection.AddModules(modules...)
require.NoError(t, err)
provider, err := collection.Build()
require.NoError(t, err)
t.Cleanup(func() {
provider.Close()
})
return provider
}
// MockDatabaseModule returns a module with a mock database
func MockDatabaseModule(users []User) godi.ModuleOption {
return godi.NewModule("mock-db",
godi.AddSingleton(func() *Database {
db := &MockDatabase{
users: make(map[string]*User),
}
for _, u := range users {
db.users[u.ID] = &u
}
return db
}),
)
}
Use the helpers:
func TestWithHelpers(t *testing.T) {
users := []User{
{ID: "1", Name: "Alice"},
{ID: "2", Name: "Bob"},
}
provider := testutil.BuildTestProvider(t,
testutil.MockDatabaseModule(users),
godi.AddScoped(NewUserService),
)
service, _ := godi.Resolve[*UserService](provider)
// Test...
}
Table-Driven Tests
func TestUserValidation(t *testing.T) {
tests := []struct {
name string
username string
email string
wantError bool
errorMsg string
}{
{
name: "valid user",
username: "alice",
email: "alice@example.com",
wantError: false,
},
{
name: "empty username",
username: "",
email: "alice@example.com",
wantError: true,
errorMsg: "username required",
},
{
name: "invalid email",
username: "alice",
email: "not-an-email",
wantError: true,
errorMsg: "invalid email",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Fresh provider for each test
provider := testutil.BuildTestProvider(t,
testutil.MockDatabaseModule(nil),
godi.AddScoped(NewUserService),
)
service, _ := godi.Resolve[*UserService](provider)
err := service.CreateUser(tt.username, tt.email)
if tt.wantError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
}
})
}
}
Testing Error Scenarios
// Module for error scenarios
func ErrorModule() godi.ModuleOption {
return godi.NewModule("errors",
godi.AddSingleton(func() *Database {
return &MockDatabase{
shouldError: true,
errorMsg: "database connection failed",
}
}),
godi.AddSingleton(func() *EmailService {
return &MockEmailService{
shouldFail: true,
}
}),
)
}
func TestErrorHandling(t *testing.T) {
provider := testutil.BuildTestProvider(t,
ErrorModule(),
godi.AddScoped(NewUserService),
)
service, _ := godi.Resolve[*UserService](provider)
err := service.CreateUser("alice", "alice@example.com")
assert.Error(t, err)
assert.Contains(t, err.Error(), "database connection failed")
}
Testing with Scopes
Test request isolation:
func TestRequestIsolation(t *testing.T) {
provider := testutil.BuildTestProvider(t,
godi.AddSingleton(NewDatabase),
godi.AddScoped(NewTransaction),
godi.AddScoped(NewUserService),
)
// Simulate two concurrent requests
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
// Request 1
scope, _ := provider.CreateScope(context.Background())
defer scope.Close()
service, _ := godi.Resolve[*UserService](scope)
tx, _ := godi.Resolve[*Transaction](scope)
// Each request has its own transaction
assert.NotNil(t, tx)
}()
go func() {
defer wg.Done()
// Request 2
scope, _ := provider.CreateScope(context.Background())
defer scope.Close()
service, _ := godi.Resolve[*UserService](scope)
tx, _ := godi.Resolve[*Transaction](scope)
// Different transaction than request 1
assert.NotNil(t, tx)
}()
wg.Wait()
}
Integration Testing
Gradually replace mocks with real services:
func IntegrationModule(useRealDB bool) godi.ModuleOption {
return godi.NewModule("integration",
godi.AddSingleton(func() *Database {
if useRealDB {
// Real database for integration tests
return NewPostgresDatabase("postgres://test...")
}
// Mock for unit tests
return &MockDatabase{}
}),
godi.AddSingleton(NewLogger), // Always use real logger
godi.AddScoped(NewUserService),
)
}
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
provider := testutil.BuildTestProvider(t,
IntegrationModule(true), // Use real database
)
service, _ := godi.Resolve[*UserService](provider)
// Test with real database...
}
Testing HTTP Handlers
func TestHTTPHandler(t *testing.T) {
provider := testutil.BuildTestProvider(t,
testutil.MockDatabaseModule([]User{
{ID: "1", Name: "Alice"},
}),
godi.AddScoped(NewUserService),
)
handler := NewUserHandler(provider)
// Create test request
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
// Call handler
handler.GetUser(w, req)
// Check response
assert.Equal(t, 200, w.Code)
var user User
json.Unmarshal(w.Body.Bytes(), &user)
assert.Equal(t, "Alice", user.Name)
}
Benchmark Testing
func BenchmarkUserService(b *testing.B) {
provider := testutil.BuildTestProvider(b,
testutil.MockDatabaseModule(nil),
godi.AddScoped(NewUserService),
)
service, _ := godi.Resolve[*UserService](provider)
b.ResetTimer()
for i := 0; i < b.N; i++ {
service.GetUser("1")
}
}
Best Practices
1. Use Interfaces for Mocking
// Define interfaces
type Database interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
// Easy to mock
type MockDatabase struct{}
func (m *MockDatabase) GetUser(id string) (*User, error) {
return &User{ID: id, Name: "Mock User"}, nil
}
2. Create Test Modules for Common Scenarios
// testutil/modules.go
var HappyPathModule = godi.NewModule("happy",
MockDatabaseModule(testUsers),
MockEmailModule(false), // success
)
var ErrorModule = godi.NewModule("error",
MockDatabaseModule(nil),
MockEmailModule(true), // fail
)
// Use in tests
provider := testutil.BuildTestProvider(t,
testutil.HappyPathModule,
godi.AddScoped(NewUserService),
)
3. Test One Thing at a Time
// Good - focused test
func TestUserService_GetUser_NotFound(t *testing.T) {
provider := testutil.BuildTestProvider(t,
testutil.MockDatabaseModule(nil), // No users
godi.AddScoped(NewUserService),
)
service, _ := godi.Resolve[*UserService](provider)
_, err := service.GetUser("999")
assert.Error(t, err)
assert.Equal(t, "user not found", err.Error())
}
4. Use t.Cleanup for Resources
func TestWithTempFile(t *testing.T) {
file, _ := os.CreateTemp("", "test")
t.Cleanup(func() {
os.Remove(file.Name())
})
provider := testutil.BuildTestProvider(t,
godi.AddSingleton(func() *FileService {
return NewFileService(file.Name())
}),
)
// Test...
}
Common Testing Patterns
Assert Mock Calls
type MockRepository struct {
calls []string
}
func (m *MockRepository) Save(entity any) error {
m.calls = append(m.calls, "Save")
return nil
}
// In test
repo := &MockRepository{}
// ... run test ...
assert.Equal(t, []string{"Save", "Save"}, repo.calls)
Test with Context
func TestWithContext(t *testing.T) {
ctx := context.WithValue(context.Background(), "userID", "123")
provider := testutil.BuildTestProvider(t,
testutil.MockDatabaseModule(nil),
godi.AddScoped(NewRequestContext), // Uses context
)
scope, _ := provider.CreateScope(ctx)
defer scope.Close()
reqCtx, _ := godi.Resolve[*RequestContext](scope)
assert.Equal(t, "123", reqCtx.UserID)
}
Summary
Testing with godi gives you:
✅ Fast tests - No real dependencies
✅ Isolated tests - Each test independent
✅ Easy mocking - Swap implementations instantly
✅ Better coverage - Easy to test edge cases
✅ Reusable setup - Test modules and helpers
The key is to use modules to organize your mocks and let godi handle the wiring!