Skip to content

Latest commit

 

History

History
217 lines (162 loc) · 4.62 KB

File metadata and controls

217 lines (162 loc) · 4.62 KB
title Testing
description How to test code that uses sum
author zoobzio
published 2026-01-26
updated 2026-01-26
tags
testing
guide
best-practices

Testing

This guide covers testing patterns for code that uses sum.

Build Tags

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 ./...

Test Helpers

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
}

Unit Testing Services

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)
    }
}

Testing Guards

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)
        }
    })
}

Testing Events

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)
    }
}

Integration Testing

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 ./...

Testing the Service Lifecycle

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)
    }
}

Best Practices

  1. Always call sum.Reset() at the start of tests that use the registry
  2. Use build tags to isolate test-only code
  3. Prefer sumtest.TestContext() for automatic timeout and cleanup
  4. Test guards separately from the services they protect
  5. Close event listeners in tests to prevent leaks

Next Steps