| title | Testing | |||
|---|---|---|---|---|
| description | How to test code that uses sum | |||
| author | zoobzio | |||
| published | 2026-01-26 | |||
| updated | 2026-01-26 | |||
| tags |
|
This guide covers testing patterns for code that uses sum.
Sum provides test-only functions behind build tags:
//go:build testing || integration
// Reset clears all registered services
sum.Reset()
// Unregister removes a specific service type
sum.Unregister[UserRepository]()Run tests with:
go test -tags testing ./...The testing package provides context helpers:
import sumtest "github.com/zoobz-io/sum/testing"
func TestSomething(t *testing.T) {
ctx := sumtest.TestContext(t) // 10 second timeout, auto-cleanup
// or
ctx := sumtest.ShortContext(t) // 1 second timeout
}For unit tests, reset the registry and register mocks:
func TestUserHandler(t *testing.T) {
sum.Reset()
k := sum.Start()
// Register a mock
sum.Register[UserRepository](k, &mockUserRepo{
users: map[string]*User{"1": {ID: "1", Name: "Test"}},
})
sum.Freeze(k)
ctx := sumtest.TestContext(t)
// Test code that uses the repository
handler := NewUserHandler()
user, err := handler.GetUser(ctx, "1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Test" {
t.Errorf("expected Test, got %s", user.Name)
}
}Test that guards correctly allow or deny access:
func TestAdminGuard(t *testing.T) {
sum.Reset()
k := sum.Start()
sum.Register[AdminService](k, &adminImpl{}).
Guard(requireRole("admin"))
sum.Freeze(k)
t.Run("allows admin", func(t *testing.T) {
ctx := contextWithRole(sumtest.TestContext(t), "admin")
_, err := sum.Use[AdminService](ctx)
if err != nil {
t.Errorf("expected access, got: %v", err)
}
})
t.Run("denies non-admin", func(t *testing.T) {
ctx := contextWithRole(sumtest.TestContext(t), "user")
_, err := sum.Use[AdminService](ctx)
if err != sum.ErrAccessDenied {
t.Errorf("expected ErrAccessDenied, got: %v", err)
}
})
}Test event emission and handling:
func TestUserCreatedEvent(t *testing.T) {
ctx := sumtest.TestContext(t)
var received *UserCreated
listener := UserCreatedEvent.Listen(func(ctx context.Context, u UserCreated) {
received = &u
})
defer listener.Close()
// Emit an event
UserCreatedEvent.Emit(ctx, UserCreated{ID: "123", Email: "test@example.com"})
// Verify it was received
if received == nil {
t.Fatal("event not received")
}
if received.ID != "123" {
t.Errorf("expected ID 123, got %s", received.ID)
}
}For integration tests, use the real implementations but isolated databases:
//go:build integration
func TestUserRepository_Integration(t *testing.T) {
sum.Reset()
// Use test database
db := setupTestDB(t)
_ = sum.Start()
database := sum.NewDatabase[User](db, "users", renderer)
// Create repository with embedded database
type UserRepository struct {
*sum.Database[User]
}
repo := &UserRepository{Database: database}
ctx := sumtest.TestContext(t)
// Test against real database
user, err := repo.Get(ctx, "123")
// ...
}Run integration tests:
go test -tags integration ./...Test startup and shutdown behavior:
func TestServiceLifecycle(t *testing.T) {
sum.Reset()
svc := sum.New(sum.ServiceConfig{Host: "localhost", Port: 0})
k := sum.Start()
sum.Freeze(k)
// Start in background
errCh := make(chan error, 1)
go func() {
errCh <- svc.Start()
}()
// Give it time to start
time.Sleep(100 * time.Millisecond)
// Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := svc.Shutdown(ctx); err != nil {
t.Errorf("shutdown error: %v", err)
}
}- Always call
sum.Reset()at the start of tests that use the registry - Use build tags to isolate test-only code
- Prefer
sumtest.TestContext()for automatic timeout and cleanup - Test guards separately from the services they protect
- Close event listeners in tests to prevent leaks
- Troubleshooting — Common issues and solutions
- API Reference — Complete function documentation