Skip to content

Local Testing

Testing modules locally before deployment helps catch issues early. This guide covers local development and testing strategies.

Running Locally

Basic Run

bash
cd my-module
tinysystems run

With Debug Output

bash
tinysystems run --debug

Watch Mode

Automatically restart on file changes:

bash
tinysystems run --watch

Force Leader Mode

Test leader-only functionality:

bash
tinysystems run --leader

Connecting to Kubernetes

With Minikube

bash
# Start minikube
minikube start

# Run module connected to cluster
tinysystems run --kubeconfig ~/.kube/config

With Kind

bash
# Create cluster
kind create cluster --name tinysystems-dev

# Run module
tinysystems run --kubeconfig ~/.kube/config --namespace dev

Port Forwarding

Access cluster services locally:

bash
# Forward another module's gRPC port
kubectl port-forward svc/common-module-v1 50052:50051 -n tinysystems

# Your module can now reach common-module at localhost:50052

Testing Components

Unit Tests

Run component tests:

bash
# All tests
go test ./...

# Specific component
go test ./components/transformer/...

# With coverage
go test -cover ./...

# With race detection
go test -race ./...

Using CLI

bash
tinysystems test --verbose --coverage

Integration Tests

Test with real Kubernetes:

go
// integration_test.go
//go:build integration

package integration

import (
    "context"
    "testing"
    "time"

    "github.com/stretchr/testify/require"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

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

    // Create test client
    k8sClient, err := client.New(config, client.Options{})
    require.NoError(t, err)

    // Create test TinyNode
    node := &v1alpha1.TinyNode{
        ObjectMeta: metav1.ObjectMeta{
            Name:      "test-node",
            Namespace: "tinysystems-test",
        },
        Spec: v1alpha1.TinyNodeSpec{
            Component: "my-component",
            Module:    "my-module",
        },
    }
    err = k8sClient.Create(context.Background(), node)
    require.NoError(t, err)

    // Wait for processing
    time.Sleep(5 * time.Second)

    // Verify status
    err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(node), node)
    require.NoError(t, err)
    require.Equal(t, "Ready", node.Status.State)
}

Run with:

bash
go test -tags=integration ./...

Manual Testing

Send Test Messages

Create a test TinySignal:

yaml
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinySignal
metadata:
  name: test-signal
  namespace: tinysystems
spec:
  node: my-node
  port: input
  data:
    text: "Hello, World!"

Apply:

bash
kubectl apply -f test-signal.yaml

Test HTTP Endpoints

If your component exposes HTTP:

bash
# Port forward to the pod
kubectl port-forward svc/my-node 8080:8080 -n tinysystems

# Send test request
curl -X POST http://localhost:8080/api/test \
  -H "Content-Type: application/json" \
  -d '{"message": "test"}'

Development Workflow

1. Write Component

go
// components/processor/component.go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == "input" {
        input := msg.(Input)
        result := c.process(input)
        return output(ctx, "output", result)
    }
    return nil
}

2. Write Tests

go
// components/processor/component_test.go
func TestProcessor_Handle(t *testing.T) {
    component := &Component{}

    var result Output
    handler := func(ctx context.Context, port string, msg any) error {
        if port == "output" {
            result = msg.(Output)
        }
        return nil
    }

    err := component.Handle(context.Background(), handler, "input", Input{
        Data: "test",
    })

    assert.NoError(t, err)
    assert.Equal(t, "processed: test", result.Data)
}

3. Run Tests

bash
go test ./components/processor/...

4. Run Locally

bash
tinysystems run --watch --debug

5. Test with Kubernetes

bash
# Apply test resources
kubectl apply -f test-fixtures/

# Check logs
tinysystems run --debug

# Verify results
kubectl get tinynodes -n tinysystems

Test Fixtures

Directory Structure

test-fixtures/
├── nodes/
│   ├── basic-node.yaml
│   └── complex-node.yaml
├── signals/
│   ├── test-input.yaml
│   └── test-control.yaml
└── expected/
    ├── basic-output.yaml
    └── complex-output.yaml

Sample Fixture

yaml
# test-fixtures/nodes/basic-node.yaml
apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
metadata:
  name: test-processor
  namespace: tinysystems-test
spec:
  component: github.com/myorg/my-module/processor
  module: my-module
  edges:
    - from: _settings
      data:
        mode: "test"
        debug: true

Debugging Locally

Enable Verbose Logging

go
import "sigs.k8s.io/controller-runtime/pkg/log/zap"

func init() {
    opts := zap.Options{
        Development: true,
        Level:       zapcore.DebugLevel,
    }
    log.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
}

Add Debug Endpoints

go
// Start debug HTTP server
go func() {
    http.HandleFunc("/debug/state", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]any{
            "settings":  c.settings,
            "isRunning": c.isRunning,
            "nodeCount": len(c.nodes),
        })
    })
    http.ListenAndServe(":6060", nil)
}()

Use Delve

bash
# Install delve
go install github.com/go-delve/delve/cmd/dlv@latest

# Debug run
dlv debug ./cmd/main.go

CI/CD Testing

GitHub Actions

yaml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'

      - name: Run tests
        run: go test -v -race -coverprofile=coverage.out ./...

      - name: Upload coverage
        uses: codecov/codecov-action@v3

Integration Tests in CI

yaml
  integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Create Kind cluster
        uses: helm/kind-action@v1

      - name: Install CRDs
        run: |
          kubectl apply -f charts/crds/

      - name: Run integration tests
        run: go test -tags=integration -v ./...

Best Practices

1. Test All Code Paths

go
func TestComponent_AllPorts(t *testing.T) {
    t.Run("settings", testSettings)
    t.Run("control", testControl)
    t.Run("input", testInput)
    t.Run("error handling", testErrors)
}

2. Use Table-Driven Tests

go
func TestProcessor(t *testing.T) {
    tests := []struct {
        name     string
        input    Input
        expected Output
    }{
        {"basic", Input{Data: "a"}, Output{Result: "A"}},
        {"empty", Input{Data: ""}, Output{Result: ""}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Test
        })
    }
}

3. Mock External Dependencies

go
type mockClient struct {
    response any
    err      error
}

func (m *mockClient) Do(req *http.Request) (*http.Response, error) {
    // Return mock response
}

4. Test with Real Context

go
ctx := context.Background()
ctx = utils.WithLeader(ctx, true)  // Simulate leader

Next Steps

Build flow-based applications on Kubernetes