zoobzio February 17, 2025 Edit this page

Testing

This guide covers strategies for testing code that uses aegis.

Test Helpers

Aegis provides test helpers in the testing subpackage. Import with the testing build tag:

//go:build testing

package mypackage_test

import (
    "testing"
    aegistesting "github.com/zoobz-io/aegis/testing"
)

Eventually

Wait for a condition with timeout:

func TestNodeStartup(t *testing.T) {
    node := createTestNode(t)
    node.StartServer()

    aegistesting.Eventually(t, func() bool {
        return node.IsHealthy()
    }, 5*time.Second, 100*time.Millisecond)
}

RequireNoError / RequireError

Assert on errors:

aegistesting.RequireNoError(t, err)
aegistesting.RequireError(t, err)

Creating Test Nodes

For unit tests, create nodes with temporary certificate directories:

func createTestNode(t *testing.T, id string) *aegis.Node {
    t.Helper()

    certDir := t.TempDir()

    node, err := aegis.NewNodeBuilder().
        WithID(id).
        WithName("Test Node " + id).
        WithAddress("localhost:0"). // Random port
        WithCertDir(certDir).
        Build()

    if err != nil {
        t.Fatalf("failed to create node: %v", err)
    }

    t.Cleanup(func() {
        node.Shutdown()
    })

    return node
}

Using localhost:0 lets the OS assign an available port.

Testing Service Providers

To test a service implementation:

func TestIdentityService(t *testing.T) {
    // Create provider node
    provider, err := aegis.NewNodeBuilder().
        WithID("provider").
        WithName("Provider").
        WithAddress("localhost:0").
        WithServices(aegis.ServiceInfo{Name: "identity", Version: "v1"}).
        WithServiceRegistration(func(s *grpc.Server) {
            identity.RegisterIdentityServiceServer(s, &myIdentityServer{})
        }).
        WithCertDir(t.TempDir()).
        Build()
    require.NoError(t, err)

    err = provider.StartServer()
    require.NoError(t, err)
    defer provider.Shutdown()

    // Create consumer node with same CA
    consumer, err := aegis.NewNodeBuilder().
        WithID("consumer").
        WithName("Consumer").
        WithAddress("localhost:0").
        WithCertDir(t.TempDir()). // Different certs won't work!
        Build()
    require.NoError(t, err)

    // ... test the service
}

Important: For nodes to communicate, they must trust the same CA. In tests, either:

  1. Share the certificate directory
  2. Use a shared test CA
  3. Pre-generate certificates

Sharing Test Certificates

Create a helper that generates a shared CA:

func setupTestCA(t *testing.T) string {
    t.Helper()
    certDir := t.TempDir()

    // Generate CA once
    _, err := aegis.LoadOrGenerateTLS("test-ca", certDir)
    require.NoError(t, err)

    return certDir
}

func createNodeWithCA(t *testing.T, id, certDir string) *aegis.Node {
    t.Helper()

    // Generate node cert using existing CA
    _, err := aegis.LoadOrGenerateTLS(id, certDir)
    require.NoError(t, err)

    node, err := aegis.NewNodeBuilder().
        WithID(id).
        WithName("Node " + id).
        WithAddress("localhost:0").
        WithCertDir(certDir).
        Build()
    require.NoError(t, err)

    return node
}

Testing Topology Sync

func TestTopologySync(t *testing.T) {
    certDir := setupTestCA(t)

    node1 := createNodeWithCA(t, "node-1", certDir)
    node2 := createNodeWithCA(t, "node-2", certDir)

    node1.StartServer()
    node2.StartServer()
    defer node1.Shutdown()
    defer node2.Shutdown()

    // Add node2 as peer of node1
    err := node1.AddPeer(aegis.PeerInfo{
        ID:      "node-2",
        Type:    aegis.NodeTypeGeneric,
        Address: node2.Address,
    })
    require.NoError(t, err)

    // Sync topology
    err = node1.SyncTopology(context.Background(), "node-2")
    require.NoError(t, err)

    // Verify both nodes appear in topology
    nodes := node1.Topology.GetAllNodes()
    assert.Len(t, nodes, 2)
}

Mocking Services

For unit tests that don't need real gRPC:

type mockIdentityClient struct {
    validateResponse *identity.ValidateSessionResponse
    validateError    error
}

func (m *mockIdentityClient) ValidateSession(ctx context.Context, req *identity.ValidateSessionRequest, opts ...grpc.CallOption) (*identity.ValidateSessionResponse, error) {
    return m.validateResponse, m.validateError
}

func TestWithMockedIdentity(t *testing.T) {
    mock := &mockIdentityClient{
        validateResponse: &identity.ValidateSessionResponse{
            Valid:  true,
            UserId: "user-123",
        },
    }

    // Use mock directly instead of ServiceClient
    resp, err := mock.ValidateSession(ctx, &identity.ValidateSessionRequest{Token: "test"})
    require.NoError(t, err)
    assert.True(t, resp.Valid)
}

Integration Tests

For end-to-end tests with real networking:

//go:build integration

func TestFullMeshCommunication(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    // Create multiple nodes
    // Start servers
    // Add peers
    // Sync topologies
    // Make service calls
    // Verify responses
}

Run with:

go test -tags=integration ./...

Next Steps