tests/

directory
v0.2.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 25, 2026 License: Apache-2.0

README

Orkestra Testing Strategy — A Complete Plan

This is your blueprint for testing Orkestra from first principles to production readiness.


The Philosophy

Test for confidence, not coverage. Every test should answer a question you're afraid to answer without it.

What breaks? → Write a test.
What must work? → Write a test.
What did we fix? → Write a test.

The Testing Pyramid

        ┌─────────────┐
        │    E2E      │  3-5 tests (slow, critical paths)
        │  (real K8s) │  What: "Does the whole thing work?"
        ├─────────────┤
        │ Integration │  10-20 tests (medium, component interactions)
        │  (fake K8s) │  What: "Do components work together?"
        ├─────────────┤
        │    Unit     │  50-100 tests (fast, logic)
        │  (pure Go)  │  What: "Does each function do what it should?"
        └─────────────┘

Phase 1: Foundation (Week 1)

1.1 Project Structure
tests/
├── unit/
│   ├── kordinator/
│   │   ├── crd_health_test.go
│   │   ├── dependency_test.go
│   │   └── activation_test.go
│   ├── katalog/
│   │   ├── merge_test.go
│   │   ├── validation_test.go
│   │   └── translator_test.go
│   ├── queue/
│   │   └── workqueue_test.go
│   └── utils/
│       ├── template_test.go
│       └── helpers_test.go
│
├── integration/
│   ├── reconciler/
│   │   ├── deployment_test.go
│   │   ├── service_test.go
│   │   └── secret_test.go
│   ├── dependency/
│   │   ├── ordering_test.go
│   │   └── cycle_detection_test.go
│   ├── activation/
│   │   └── missing_crd_test.go
│   └── komposer/
│       ├── merge_test.go
│       └── sources_test.go
│
├── e2e/
│   ├── website/
│   │   └── test.sh
│   ├── dependencies/
│   │   └── test.sh
│   ├── activation/
│   │   └── test.sh
│   └── komposer/
│       └── test.sh
│
├── fixtures/
│   ├── katalogs/
│   │   ├── website.yaml
│   │   ├── dependencies.yaml
│   │   └── komposer.yaml
│   └── crds/
│       ├── website-crd.yaml
│       └── orkapp-crd.yaml
│
├── helpers/
│   ├── fake_kubeclient.go
│   ├── fake_informer.go
│   └── testutils.go
│
├── Makefile
└── README.md
1.2 Test Helpers

Create reusable test helpers first:

// tests/helpers/fake_kubeclient.go
package helpers

import (
    "context"
    "k8s.io/client-go/kubernetes/fake"
    "github.com/orkspace/orkestra/pkg/kubeclient"
)

func NewFakeKubeclient() *kubeclient.Kubeclient {
    return &kubeclient.Kubeclient{
        Clientset: fake.NewSimpleClientset(),
    }
}

func NewFakeKubeclientWithObjects(objects ...runtime.Object) *kubeclient.Kubeclient {
    return &kubeclient.Kubeclient{
        Clientset: fake.NewSimpleClientset(objects...),
    }
}
// tests/helpers/testutils.go
package helpers

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func AssertNoError(t *testing.T, err error) {
    require.NoError(t, err)
}

func AssertError(t *testing.T, err error, msg string) {
    require.Error(t, err)
    assert.Contains(t, err.Error(), msg)
}

Phase 2: Unit Tests (Week 1-2)

2.1 CRD Health Tests
// tests/unit/kordinator/crd_health_test.go
package kordinator_test

import (
    "fmt"
    "testing"
    "time"
    
    "github.com/orkspace/orkestra/pkg/kordinator"
    "github.com/stretchr/testify/assert"
)

func TestCRDHealth_InitialState(t *testing.T) {
    h := kordinator.NewCRDHealth("test")
    
    assert.False(t, h.IsHealthy())
    assert.False(t, h.Started())
    assert.Equal(t, int64(0), h.TotalReconciles())
    assert.Equal(t, int64(0), h.FailedReconciles())
    assert.Equal(t, float64(0), h.ErrorRate())
}

func TestCRDHealth_RecordSuccess(t *testing.T) {
    h := kordinator.NewCRDHealth("test")
    
    h.RecordSuccess()
    
    assert.True(t, h.IsHealthy())
    assert.Equal(t, int64(1), h.TotalReconciles())
    assert.Equal(t, int64(0), h.FailedReconciles())
    assert.Equal(t, int64(0), h.ConsecutiveFails())
}

func TestCRDHealth_RecordFailure(t *testing.T) {
    h := kordinator.NewCRDHealth("test")
    
    h.RecordFailure(fmt.Errorf("something went wrong"), 3)
    
    assert.False(t, h.IsHealthy())
    assert.Equal(t, int64(1), h.TotalReconciles())
    assert.Equal(t, int64(1), h.FailedReconciles())
    assert.Equal(t, int64(1), h.ConsecutiveFails())
    assert.Equal(t, "something went wrong", h.LastError())
}

func TestCRDHealth_RecordFailureExceedsThreshold(t *testing.T) {
    h := kordinator.NewCRDHealth("test")
    
    // First failure — still healthy (threshold=3)
    h.RecordFailure(fmt.Errorf("error 1"), 3)
    assert.True(t, h.IsHealthy())
    
    // Second failure — still healthy
    h.RecordFailure(fmt.Errorf("error 2"), 3)
    assert.True(t, h.IsHealthy())
    
    // Third failure — becomes degraded
    h.RecordFailure(fmt.Errorf("error 3"), 3)
    assert.False(t, h.IsHealthy())
}

func TestCRDHealth_SuccessResetsConsecutiveFails(t *testing.T) {
    h := kordinator.NewCRDHealth("test")
    
    h.RecordFailure(fmt.Errorf("error"), 3)
    h.RecordFailure(fmt.Errorf("error"), 3)
    assert.Equal(t, int64(2), h.ConsecutiveFails())
    
    h.RecordSuccess()
    assert.Equal(t, int64(0), h.ConsecutiveFails())
    assert.True(t, h.IsHealthy())
}

func TestCRDHealth_ErrorRate(t *testing.T) {
    h := kordinator.NewCRDHealth("test")
    
    h.RecordSuccess()  // 1 total, 0 failed
    assert.Equal(t, float64(0), h.ErrorRate())
    
    h.RecordFailure(fmt.Errorf("error"), 3)  // 2 total, 1 failed
    assert.Equal(t, 0.5, h.ErrorRate())
    
    h.RecordFailure(fmt.Errorf("error"), 3)  // 3 total, 2 failed
    assert.InDelta(t, 0.666, h.ErrorRate(), 0.001)
}
2.2 Dependency Graph Tests
// tests/unit/kordinator/dependency_test.go
package kordinator_test

import (
    "testing"
    
    "github.com/orkspace/orkestra/pkg/katalog"
    "github.com/orkspace/orkestra/pkg/orktypes"
    "github.com/stretchr/testify/assert"
)

func TestDependencyGraph_StartupOrder(t *testing.T) {
    crds := []orktypes.CRDEntry{
        {Name: "A", DependsOn: []string{}},
        {Name: "B", DependsOn: []string{"A"}},
        {Name: "C", DependsOn: []string{"B"}},
    }
    
    graph := katalog.NewDependencyGraph(crds)
    order := graph.StartupOrder()
    
    assert.Equal(t, []string{"A", "B", "C"}, order)
}

func TestDependencyGraph_ReverseShutdownOrder(t *testing.T) {
    crds := []orktypes.CRDEntry{
        {Name: "A", DependsOn: []string{}},
        {Name: "B", DependsOn: []string{"A"}},
        {Name: "C", DependsOn: []string{"B"}},
    }
    
    graph := katalog.NewDependencyGraph(crds)
    shutdownOrder := graph.ShutdownOrder()
    
    // Should be reverse of startup order
    assert.Equal(t, []string{"C", "B", "A"}, shutdownOrder)
}

func TestDependencyGraph_DetectsCycle(t *testing.T) {
    crds := []orktypes.CRDEntry{
        {Name: "A", DependsOn: []string{"B"}},
        {Name: "B", DependsOn: []string{"A"}},
    }
    
    graph := katalog.NewDependencyGraph(crds)
    err := graph.Validate()
    
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "cycle")
}

func TestDependencyGraph_MissingDependency(t *testing.T) {
    crds := []orktypes.CRDEntry{
        {Name: "A", DependsOn: []string{"B"}},
    }
    
    graph := katalog.NewDependencyGraph(crds)
    err := graph.Validate()
    
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "not found")
}

func TestDependencyGraph_GetDependents(t *testing.T) {
    crds := []orktypes.CRDEntry{
        {Name: "A", DependsOn: []string{}},
        {Name: "B", DependsOn: []string{"A"}},
        {Name: "C", DependsOn: []string{"A"}},
    }
    
    graph := katalog.NewDependencyGraph(crds)
    dependents := graph.GetDependents("A")
    
    assert.ElementsMatch(t, []string{"B", "C"}, dependents)
}
2.3 Katalog Merge Tests
// tests/unit/katalog/merge_test.go
package katalog_test

import (
    "testing"
    
    "github.com/orkspace/orkestra/pkg/katalog"
    "github.com/orkspace/orkestra/pkg/orktypes"
    "github.com/stretchr/testify/assert"
)

func TestMerge_SingleSource(t *testing.T) {
    source1 := []orktypes.CRDEntry{
        {Name: "website", Workers: 2},
    }
    
    merger := katalog.NewMerger()
    result := merger.Merge(source1, nil)
    
    assert.Len(t, result, 1)
    assert.Equal(t, "website", result[0].Name)
    assert.Equal(t, 2, result[0].Workers)
}

func TestMerge_MultipleSources(t *testing.T) {
    source1 := []orktypes.CRDEntry{
        {Name: "website", Workers: 2},
    }
    source2 := []orktypes.CRDEntry{
        {Name: "database", Workers: 3},
    }
    
    merger := katalog.NewMerger()
    result := merger.Merge(source1, source2)
    
    assert.Len(t, result, 2)
}

func TestMerge_DuplicateNameError(t *testing.T) {
    source1 := []orktypes.CRDEntry{
        {Name: "website", Workers: 2},
    }
    source2 := []orktypes.CRDEntry{
        {Name: "website", Workers: 4},
    }
    
    merger := katalog.NewMerger()
    err := merger.MergeWithError(source1, source2)
    
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "duplicate")
}

func TestMerge_InlineOverride(t *testing.T) {
    source := []orktypes.CRDEntry{
        {Name: "website", Workers: 2},
    }
    override := []orktypes.CRDEntry{
        {Name: "website", Workers: 4},
    }
    
    merger := katalog.NewMerger()
    result := merger.MergeWithOverride(source, override)
    
    assert.Len(t, result, 1)
    assert.Equal(t, 4, result[0].Workers)
}

Phase 3: Integration Tests (Week 2-3)

3.1 Reconciler Tests
// tests/integration/reconciler/deployment_test.go
package reconciler_test

import (
    "context"
    "testing"
    
    "github.com/orkspace/orkestra/pkg/kubeclient"
    "github.com/orkspace/orkestra/pkg/reconciler"
    "github.com/orkspace/orkestra/pkg/orktypes"
    "github.com/orkspace/orkestra/tests/helpers"
    "github.com/stretchr/testify/assert"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestGenericReconciler_CreatesDeployment(t *testing.T) {
    // Setup fake client
    fakeKube := helpers.NewFakeKubeclient()
    
    // Create CRD entry with Deployment template
    crd := &orktypes.CRDEntry{
        Name: "website",
        ReconcilerConfig: orktypes.ReconcilerConfig{
            OnCreate: &orktypes.HookTemplates{
                Deployments: []orktypes.DeploymentTemplateSource{
                    {
                        Name:     "test-deployment",
                        Image:    "nginx:latest",
                        Replicas: "3",
                        Port:     "80",
                    },
                },
            },
        },
    }
    
    // Create reconciler
    reconciler := helpers.NewTestReconciler(crd, fakeKube)
    
    // Reconcile
    err := reconciler.Reconcile(context.Background(), "default/test-site")
    
    // Verify
    assert.NoError(t, err)
    
    deployment, err := fakeKube.Clientset.AppsV1().Deployments("default").Get(
        context.Background(), "test-deployment", metav1.GetOptions{})
    assert.NoError(t, err)
    assert.Equal(t, "nginx:latest", deployment.Spec.Template.Spec.Containers[0].Image)
}

func TestGenericReconciler_UpdatesDeploymentOnDrift(t *testing.T) {
    fakeKube := helpers.NewFakeKubeclientWithObjects(
        // Pre-existing deployment with wrong image
        &appsv1.Deployment{
            ObjectMeta: metav1.ObjectMeta{Name: "test-deployment", Namespace: "default"},
            Spec: appsv1.DeploymentSpec{
                Template: corev1.PodTemplateSpec{
                    Spec: corev1.PodSpec{
                        Containers: []corev1.Container{{Image: "old-image"}},
                    },
                },
            },
        },
    )
    
    crd := &orktypes.CRDEntry{
        Name: "website",
        ReconcilerConfig: orktypes.ReconcilerConfig{
            OnReconcile: &orktypes.HookTemplates{
                Deployments: []orktypes.DeploymentTemplateSource{
                    {Name: "test-deployment", Image: "nginx:latest", Replicas: "3", Reconcile: true},
                },
            },
        },
    }
    
    reconciler := helpers.NewTestReconciler(crd, fakeKube)
    err := reconciler.Reconcile(context.Background(), "default/test-site")
    
    assert.NoError(t, err)
    
    deployment, _ := fakeKube.Clientset.AppsV1().Deployments("default").Get(
        context.Background(), "test-deployment", metav1.GetOptions{})
    assert.Equal(t, "nginx:latest", deployment.Spec.Template.Spec.Containers[0].Image)
}
3.2 Activation Tests
// tests/integration/activation/missing_crd_test.go
package activation_test

import (
    "context"
    "testing"
    "time"
    
    "github.com/orkspace/orkestra/pkg/kordinator"
    "github.com/orkspace/orkestra/tests/helpers"
    "github.com/stretchr/testify/assert"
)

func TestActivation_MissingCRDAppearsLater(t *testing.T) {
    // Setup: CRD missing
    fakeKube := helpers.NewFakeKubeclient()
    katalog := helpers.NewTestKatalogWithMissingCRD("missing-crd")
    
    kordinator := kordinator.NewDependencyKordinator(fakeKube, katalog)
    
    // Start controller (CRD missing, should not start workers)
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
    go kordinator.RunOrDie(ctx)
    time.Sleep(2 * time.Second)
    
    // Verify workers not started
    assert.False(t, kordinator.IsStarted("missing-crd"))
    
    // Simulate CRD appearing
    helpers.InstallCRD(fakeKube, "missing-crd")
    
    // Wait for retry loop to detect and activate
    time.Sleep(5 * time.Second)
    
    // Verify workers started
    assert.True(t, kordinator.IsStarted("missing-crd"))
}
3.3 Komposer Tests
// tests/integration/komposer/merge_test.go
package komposer_test

import (
    "testing"
    
    "github.com/orkspace/orkestra/pkg/katalog"
    "github.com/stretchr/testify/assert"
)

func TestKomposer_MergesFileSources(t *testing.T) {
    komposer := &katalog.Komposer{
        Sources: katalog.KomposerSources{
            Files: []string{
                "./fixtures/katalogs/website.yaml",
                "./fixtures/katalogs/platform.yaml",
            },
        },
    }
    
    result, err := katalog.MergeKomposer(komposer)
    assert.NoError(t, err)
    assert.Len(t, result.CRDs, 2)
}

func TestKomposer_MergesHelmSources(t *testing.T) {
    komposer := &katalog.Komposer{
        Sources: katalog.KomposerSources{
            Helm: []katalog.HelmSource{
                {
                    Repo:    "./fixtures/charts",
                    Chart:   "test-chart",
                    Version: "0.1.0",
                },
            },
        },
    }
    
    result, err := katalog.MergeKomposer(komposer)
    assert.NoError(t, err)
    assert.NotEmpty(t, result.CRDs)
}

Phase 4: E2E Tests (Week 3-4)

4.1 E2E Test Framework

Create a reusable test harness:

#!/bin/bash
# tests/e2e/run.sh
set -e

TEST_NAME=$1
TIMEOUT=${2:-60}

echo "=== Running E2E Test: $TEST_NAME ==="

# Setup
kind create cluster --name orkestra-test
trap "kind delete cluster --name orkestra-test" EXIT

# Build Orkestra
go build -o /tmp/ork ./cmd/ork

# Run test
./tests/e2e/$TEST_NAME/test.sh

echo "=== E2E Test Passed ==="
4.2 Website E2E Test
#!/bin/bash
# tests/e2e/website/test.sh
set -e

echo "Testing: Website CRD → Deployment + Service"

# Install CRD
kubectl apply -f examples/website/website-crd.yaml

# Start Orkestra
/tmp/ork run --katalog examples/website/website-katalog.yaml &
ORK_PID=$!
sleep 5

# Apply CR
kubectl apply -f examples/website/website-cr.yaml
sleep 5

# Verify Deployment
kubectl get deployment test-website -n default
DEPLOYMENT_EXISTS=$?

# Verify Service
kubectl get service test-website-svc -n default
SERVICE_EXISTS=$?

# Verify Health Endpoint
curl -s localhost:8080/katalog/website/health | jq -e '.healthy == true'
HEALTH_OK=$?

# Verify Metrics
curl -s localhost:8080/metrics | grep controller_reconcile_total
METRICS_EXIST=$?

# Cleanup
kill $ORK_PID

if [ $DEPLOYMENT_EXISTS -eq 0 ] && [ $SERVICE_EXISTS -eq 0 ] && [ $HEALTH_OK -eq 0 ] && [ $METRICS_EXIST -eq 0 ]; then
    echo "✅ Website E2E test passed"
    exit 0
else
    echo "❌ Website E2E test failed"
    exit 1
fi
4.3 Activation E2E Test
#!/bin/bash
# tests/e2e/activation/test.sh
set -e

echo "Testing: Missing CRD appears after startup"

# Start Orkestra WITHOUT installing CRD
/tmp/ork run --katalog tests/fixtures/katalogs/missing-crd.yaml &
ORK_PID=$!
sleep 5

# Verify health shows degraded
curl -s localhost:8080/katalog/missing-crd/health | jq -e '.healthy == false'
HEALTH_DEGRADED=$?

# Install CRD
kubectl apply -f tests/fixtures/crds/missing-crd.yaml
sleep 10

# Verify health becomes healthy
curl -s localhost:8080/katalog/missing-crd/health | jq -e '.healthy == true'
HEALTH_HEALTHY=$?

# Cleanup
kill $ORK_PID
kubectl delete -f tests/fixtures/crds/missing-crd.yaml

if [ $HEALTH_DEGRADED -eq 0 ] && [ $HEALTH_HEALTHY -eq 0 ]; then
    echo "✅ Activation E2E test passed"
    exit 0
else
    echo "❌ Activation E2E test failed"
    exit 1
fi
4.4 Dependencies E2E Test
#!/bin/bash
# tests/e2e/dependencies/test.sh
set -e

echo "Testing: Dependency ordering"

# Install CRDs (A and B, where B depends on A)
kubectl apply -f tests/fixtures/crds/crd-a.yaml
kubectl apply -f tests/fixtures/crds/crd-b.yaml

# Start Orkestra
/tmp/ork run --katalog tests/fixtures/katalogs/dependencies.yaml &
ORK_PID=$!
sleep 5

# Check startup order (B should start only after A)
curl -s localhost:8080/katalog/crd-a/health | jq -e '.started == true'
A_STARTED=$?
curl -s localhost:8080/katalog/crd-b/health | jq -e '.started == true'
B_STARTED=$?

# Cleanup
kill $ORK_PID
kubectl delete -f tests/fixtures/crds/crd-a.yaml
kubectl delete -f tests/fixtures/crds/crd-b.yaml

if [ $A_STARTED -eq 0 ] && [ $B_STARTED -eq 0 ]; then
    echo "✅ Dependencies E2E test passed"
    exit 0
else
    echo "❌ Dependencies E2E test failed"
    exit 1
fi

Phase 5: Test Automation (Week 4)

5.1 Makefile
# Makefile
.PHONY: test test-unit test-integration test-e2e test-all

test: test-unit test-integration

test-unit:
	@echo "Running unit tests..."
	go test ./tests/unit/... -v -short

test-integration:
	@echo "Running integration tests..."
	go test ./tests/integration/... -v -tags=integration

test-e2e:
	@echo "Running E2E tests..."
	./tests/e2e/run.sh website
	./tests/e2e/run.sh activation
	./tests/e2e/run.sh dependencies

test-all: test-unit test-integration test-e2e

test-coverage:
	@echo "Generating coverage report..."
	go test ./... -coverprofile=coverage.out
	go tool cover -html=coverage.out -o coverage.html
	@echo "Coverage report: coverage.html"
5.2 GitHub Actions
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Unit Tests
        run: make test-unit

  integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Integration Tests
        run: make test-integration

  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - name: Setup kind
        uses: helm/kind-action@v1
      - name: E2E Tests
        run: make test-e2e

Phase 6: Test Fixtures

6.1 Sample Katalogs
# tests/fixtures/katalogs/website.yaml
apiVersion: orkestra.orkspace.io/v1
kind: Katalog
metadata:
  name: website-katalog
spec:
  crds:
    - name: website
      apiTypes:
        group: demo.orkestra.io
        version: v1alpha1
        kind: Website
        plural: websites
      operatorBox:
        default: true
        onCreate:
          deployments:
            - image: "nginx:latest"
              replicas: "3"
              reconcile: true
6.2 Sample CRDs
# tests/fixtures/crds/website-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: websites.demo.orkestra.io
spec:
  group: demo.orkestra.io
  names:
    kind: Website
    plural: websites
    singular: website
  scope: Namespaced
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                image:
                  type: string
                replicas:
                  type: integer

What Tests You Need for v1

Priority Test Type Purpose
P0 Website example works E2E The "hello world" must work
P0 Missing CRD activation Integration Core value: CRDs appear later
P0 Dependency ordering Unit Core feature
P1 Health tracking Unit Correct health transitions
P1 Katalog merge Unit Komposer correctness
P1 Reconciler creates resources Integration Templates work
P2 Drift correction Integration reconcile:true works
P2 Graceful shutdown E2E No panic on exit
P3 Metrics emission Integration Prometheus works
P3 Secret copy Integration FromSecret pattern

Success Criteria for v1

Metric Target
Unit test coverage 70% on core packages
Integration tests 10 passing
E2E tests 3 passing (website, activation, dependencies)
CI time < 5 minutes
E2E time < 2 minutes per test

The Bottom Line

Start with the Website E2E test. That's your anchor. Then add unit tests for the logic you're most nervous about. Then add integration tests for the flows that broke during development.

You don't need 100% coverage. You need confidence. This plan gives you that. 🎼

Directories

Path Synopsis
tests/helpers/fake_kubeclient.go
tests/helpers/fake_kubeclient.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL