Testing Components
Testing TinySystems components ensures reliability and correctness. This guide covers unit testing, integration testing, and testing best practices.
Unit Testing
Basic Component Test
Test the Handle() method directly:
go
package mycomponent_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tiny-systems/module/api/v1alpha1"
"github.com/tiny-systems/module/module"
)
func TestUppercaser_Handle(t *testing.T) {
component := &Uppercaser{}
// Track outputs
var outputs []struct {
port string
msg any
}
handler := func(ctx context.Context, port string, msg any) error {
outputs = append(outputs, struct {
port string
msg any
}{port, msg})
return nil
}
// Test input handling
err := component.Handle(context.Background(), handler, "input", Input{
Text: "hello world",
})
assert.NoError(t, err)
assert.Len(t, outputs, 1)
assert.Equal(t, "output", outputs[0].port)
result := outputs[0].msg.(Output)
assert.Equal(t, "HELLO WORLD", result.Text)
}Testing Settings
go
func TestComponent_Settings(t *testing.T) {
component := &MyComponent{}
// Apply settings
settings := Settings{
Timeout: 5000,
Retries: 3,
}
err := component.Handle(context.Background(), nil, v1alpha1.SettingsPort, settings)
assert.NoError(t, err)
// Verify settings applied
assert.Equal(t, 5000, component.settings.Timeout)
assert.Equal(t, 3, component.settings.Retries)
}Testing with Mock Handler
go
type mockHandler struct {
calls []struct {
port string
msg any
}
returnErr error
}
func (m *mockHandler) Handle(ctx context.Context, port string, msg any) error {
m.calls = append(m.calls, struct {
port string
msg any
}{port, msg})
return m.returnErr
}
func TestComponent_MultipleOutputs(t *testing.T) {
component := &Router{}
mock := &mockHandler{}
// Set up settings first
component.Handle(context.Background(), mock.Handle, v1alpha1.SettingsPort, Settings{
Routes: []string{"route_a", "route_b"},
})
// Test routing
err := component.Handle(context.Background(), mock.Handle, "input", Message{
Type: "a",
Data: "test",
})
assert.NoError(t, err)
assert.Len(t, mock.calls, 1)
assert.Equal(t, "route_a", mock.calls[0].port)
}Testing Control Ports
Testing Button Clicks
go
func TestTicker_Control(t *testing.T) {
ticker := &Ticker{}
// Apply settings
ticker.Handle(context.Background(), nil, v1alpha1.SettingsPort, Settings{
Delay: 100,
})
// Simulate leader context
ctx := utils.WithLeader(context.Background(), true)
// Start ticker
var started bool
handler := func(ctx context.Context, port string, msg any) error {
if port == "output" {
started = true
}
return nil
}
go func() {
ticker.Handle(ctx, handler, v1alpha1.ControlPort, Control{Start: true})
}()
// Wait for at least one tick
time.Sleep(150 * time.Millisecond)
assert.True(t, started)
// Stop ticker
ticker.Handle(ctx, handler, v1alpha1.ControlPort, Control{Stop: true})
}Testing Leader-Only Behavior
go
func TestComponent_LeaderOnly(t *testing.T) {
component := &LeaderComponent{}
t.Run("leader processes control", func(t *testing.T) {
ctx := utils.WithLeader(context.Background(), true)
var processed bool
handler := func(ctx context.Context, port string, msg any) error {
processed = true
return nil
}
err := component.Handle(ctx, handler, v1alpha1.ControlPort, Control{Action: true})
assert.NoError(t, err)
assert.True(t, processed)
})
t.Run("non-leader ignores control", func(t *testing.T) {
ctx := utils.WithLeader(context.Background(), false)
var processed bool
handler := func(ctx context.Context, port string, msg any) error {
processed = true
return nil
}
err := component.Handle(ctx, handler, v1alpha1.ControlPort, Control{Action: true})
assert.NoError(t, err)
assert.False(t, processed)
})
}Testing Error Handling
go
func TestComponent_ErrorHandling(t *testing.T) {
component := &Processor{}
t.Run("handles invalid input", func(t *testing.T) {
err := component.Handle(context.Background(), nil, "input", "invalid")
assert.Error(t, err)
})
t.Run("routes to error port", func(t *testing.T) {
var errorOutput *ErrorOutput
handler := func(ctx context.Context, port string, msg any) error {
if port == "error" {
errorOutput = msg.(*ErrorOutput)
}
return nil
}
err := component.Handle(context.Background(), handler, "input", Input{
Data: "invalid-data",
})
assert.NoError(t, err) // Error handled via port
assert.NotNil(t, errorOutput)
assert.Contains(t, errorOutput.Error, "invalid")
})
}Testing Context Cancellation
go
func TestComponent_Cancellation(t *testing.T) {
component := &LongProcessor{}
ctx, cancel := context.WithCancel(context.Background())
var completed bool
handler := func(ctx context.Context, port string, msg any) error {
completed = true
return nil
}
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
err := component.Handle(ctx, handler, "input", Input{
Items: make([]string, 1000), // Large input
})
assert.ErrorIs(t, err, context.Canceled)
assert.False(t, completed)
}Testing Reconciliation
go
func TestComponent_Reconcile(t *testing.T) {
component := &Server{}
node := v1alpha1.TinyNode{
ObjectMeta: metav1.ObjectMeta{
Name: "server-abc123",
},
Status: v1alpha1.TinyNodeStatus{
Metadata: map[string]string{
"http-server-port": "8080",
},
},
}
handler := func(ctx context.Context, port string, msg any) error {
return nil
}
err := component.Handle(context.Background(), handler, v1alpha1.ReconcilePort, node)
assert.NoError(t, err)
// Verify component state
assert.Equal(t, "server-abc123", component.nodeName)
assert.Equal(t, 8080, component.currentPort)
}Integration Testing
Testing with Real Scheduler
go
func TestComponent_Integration(t *testing.T) {
// Create a test scheduler
scheduler := runner.NewScheduler()
// Register test component
component := &MyComponent{}
scheduler.Register("test-node", component)
// Apply settings
scheduler.Handle(context.Background(), runner.Msg{
To: "test-node._settings",
Data: Settings{Enabled: true},
})
// Send test message
result, err := scheduler.Handle(context.Background(), runner.Msg{
To: "test-node.input",
Data: Input{Value: "test"},
})
assert.NoError(t, err)
assert.NotNil(t, result)
}Testing Edge Evaluation
go
func TestEdge_DataTransformation(t *testing.T) {
evaluator := expression.NewEvaluator()
template := map[string]any{
"name": "{{$.firstName + ' ' + $.lastName}}",
"upper": "{{$.name.toUpperCase()}}",
}
source := map[string]any{
"firstName": "John",
"lastName": "Doe",
"name": "john",
}
result, err := evaluator.Evaluate(template, source)
assert.NoError(t, err)
resultMap := result.(map[string]any)
assert.Equal(t, "John Doe", resultMap["name"])
assert.Equal(t, "JOHN", resultMap["upper"])
}Table-Driven Tests
go
func TestTransformer_Handle(t *testing.T) {
tests := []struct {
name string
input Input
expected Output
hasError bool
}{
{
name: "simple text",
input: Input{Text: "hello"},
expected: Output{Text: "HELLO"},
},
{
name: "empty text",
input: Input{Text: ""},
expected: Output{Text: ""},
},
{
name: "special characters",
input: Input{Text: "hello-world_123"},
expected: Output{Text: "HELLO-WORLD_123"},
},
{
name: "unicode",
input: Input{Text: "héllo"},
expected: Output{Text: "HÉLLO"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
component := &Uppercaser{}
var result Output
handler := func(ctx context.Context, port string, msg any) error {
result = msg.(Output)
return nil
}
err := component.Handle(context.Background(), handler, "input", tt.input)
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}Benchmarking
go
func BenchmarkComponent_Handle(b *testing.B) {
component := &Processor{}
// Setup
component.Handle(context.Background(), nil, v1alpha1.SettingsPort, Settings{})
handler := func(ctx context.Context, port string, msg any) error {
return nil
}
input := Input{Data: "benchmark data"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
component.Handle(context.Background(), handler, "input", input)
}
}
func BenchmarkComponent_Parallel(b *testing.B) {
component := &Processor{}
component.Handle(context.Background(), nil, v1alpha1.SettingsPort, Settings{})
handler := func(ctx context.Context, port string, msg any) error {
return nil
}
b.RunParallel(func(pb *testing.PB) {
input := Input{Data: "parallel data"}
for pb.Next() {
component.Handle(context.Background(), handler, "input", input)
}
})
}Test Utilities
Helper Functions
go
// testutil/helpers.go
func NewTestHandler() (*TestHandler, func() []TestCall) {
h := &TestHandler{}
return h, func() []TestCall { return h.Calls }
}
type TestCall struct {
Port string
Msg any
}
type TestHandler struct {
Calls []TestCall
Err error
}
func (h *TestHandler) Handle(ctx context.Context, port string, msg any) error {
h.Calls = append(h.Calls, TestCall{Port: port, Msg: msg})
return h.Err
}
func LeaderContext() context.Context {
return utils.WithLeader(context.Background(), true)
}
func ReaderContext() context.Context {
return utils.WithLeader(context.Background(), false)
}Usage
go
func TestWithHelpers(t *testing.T) {
component := &MyComponent{}
handler, getCalls := testutil.NewTestHandler()
ctx := testutil.LeaderContext()
component.Handle(ctx, handler.Handle, "input", Input{})
calls := getCalls()
assert.Len(t, calls, 1)
}Best Practices
1. Test All Ports
go
func TestComponent_AllPorts(t *testing.T) {
t.Run("settings", testSettings)
t.Run("control", testControl)
t.Run("reconcile", testReconcile)
t.Run("input", testInput)
t.Run("unknown port", testUnknownPort)
}2. Test State Transitions
go
func TestComponent_StateTransitions(t *testing.T) {
component := &StatefulComponent{}
// Initial state
assert.Equal(t, StateIdle, component.state)
// Start
component.Handle(ctx, handler, v1alpha1.ControlPort, Control{Start: true})
assert.Equal(t, StateRunning, component.state)
// Stop
component.Handle(ctx, handler, v1alpha1.ControlPort, Control{Stop: true})
assert.Equal(t, StateStopped, component.state)
}3. Test Concurrent Access
go
func TestComponent_Concurrent(t *testing.T) {
component := &ThreadSafeComponent{}
handler := func(ctx context.Context, port string, msg any) error { return nil }
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
component.Handle(context.Background(), handler, "input", Input{ID: i})
}(i)
}
wg.Wait()
}Next Steps
- Component Patterns - Patterns to test
- Debugging - Debug failing tests
- Observability - Monitor in production