Testing¶
Comprehensive testing guide for Language Operator.
Test Categories¶
Unit Tests¶
Location: src/controllers/*_test.go, src/pkg/**/*_test.go
Run:
Characteristics:
- Use fake Kubernetes client (
controller-runtime/pkg/client/fake) - Fast, no external dependencies
- Test controller logic in isolation
Example:
func TestLanguageAgentController(t *testing.T) {
scheme := testutil.SetupTestScheme(t)
agent := gen.LanguageAgent("test-agent", "default")
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(agent).
WithStatusSubresource(agent).
Build()
reconciler := &LanguageAgentReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
Recorder: record.NewFakeRecorder(100),
EventManager: events.NewEventManager(record.NewFakeRecorder(100)),
RegistryManager: &mockRegistryManager{},
NetworkIsolationEnabled: false,
}
// mockRegistryManager is defined in languageagent_controller_test.go — copy it into your test file.
// First reconcile adds finalizer
_, err := reconciler.Reconcile(ctx, req)
require.NoError(t, err)
// Second reconcile creates resources
_, err = reconciler.Reconcile(ctx, req)
require.NoError(t, err)
}
Integration Tests¶
Location: src/controllers/*_integration_test.go
Run:
Characteristics:
- Use
//go:build integrationbuild tag - Run against real Kubernetes API server (envtest)
- Test full reconciliation loops with CRD validation
- Slower but more realistic
Setup:
Integration tests use controller-runtime's envtest which runs a real etcd and kube-apiserver:
//go:build integration
var _ = Describe("LanguageAgent Controller", func() {
It("Should create deployment", func() {
agent := &v1alpha1.LanguageAgent{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha1.LanguageAgentSpec{
Image: "test:latest",
},
}
Expect(k8sClient.Create(ctx, agent)).Should(Succeed())
Eventually(func() error {
deployment := &appsv1.Deployment{}
return k8sClient.Get(ctx, types.NamespacedName{
Name: agent.Name,
Namespace: agent.Namespace,
}, deployment)
}, timeout, interval).Should(Succeed())
})
})
End-to-End Tests¶
Status: Not yet implemented
Planned:
- Deploy operator to real cluster
- Create CRD instances
- Verify full functionality
- Test upgrades and migrations
Testing Patterns¶
Two-Reconcile Pattern¶
Controllers always require two reconcile calls in tests:
// First reconcile: adds finalizer
result, err := reconciler.Reconcile(ctx, req)
require.NoError(t, err)
require.False(t, result.Requeue)
// Second reconcile: creates resources
result, err = reconciler.Reconcile(ctx, req)
require.NoError(t, err)
Fluent Fixture Builders¶
Use builders from internal/testutil/gen/:
agent := gen.LanguageAgent("my-agent", "default",
gen.WithImage("test:latest"),
gen.WithModelRef("claude-sonnet"),
gen.WithInstructions("Do something useful"),
)
Event Validation¶
Verify events are recorded:
recorder := record.NewFakeRecorder(100)
reconciler := &LanguageAgentReconciler{
Client: fakeClient,
Scheme: scheme,
Log: logr.Discard(),
Recorder: recorder,
EventManager: events.NewEventManager(recorder),
RegistryManager: &mockRegistryManager{},
NetworkIsolationEnabled: false,
}
// ... reconcile ...
select {
case event := <-recorder.Events:
assert.Contains(t, event, "ResourceCreated")
default:
t.Fatal("Expected event not recorded")
}
Status Condition Checks¶
var agent v1alpha1.LanguageAgent
err := fakeClient.Get(ctx, req.NamespacedName, &agent)
require.NoError(t, err)
condition := meta.FindStatusCondition(agent.Status.Conditions, "Ready")
require.NotNil(t, condition)
assert.Equal(t, metav1.ConditionTrue, condition.Status)
CI Testing¶
Test Workflow¶
File: .github/workflows/test.yaml
Jobs:
- lint - gofmt and go vet
- unit-test - All unit tests with coverage
- integration-test - Integration tests with envtest
- validate-manifests - CRD generation validation
Runs on:
- Every push to
main - Every pull request
- Manual workflow dispatch
Coverage¶
Unit test coverage is reported to CI:
Manual Testing¶
Local Cluster Testing¶
-
Create k3d cluster:
-
Install operator from source:
-
Watch logs:
-
Check resources:
Testing CRD Changes¶
After modifying CRD types:
-
Regenerate:
-
Reinstall CRDs:
-
Test with sample resources
Test Data¶
No Mock Data Rule¶
Critical: Features must work with real data before commit.
- No mock telemetry data
- No fake Kubernetes responses (except in unit tests)
- No stubbed external services in integration tests
Use real:
- Kubernetes API for integration tests
- Actual model proxies for e2e tests
Debugging Tests¶
Verbose Output¶
Run Single Test¶
Integration Test Debugging¶
Check Test Setup¶
# Verify envtest is installed
setup-envtest list
# Use specific Kubernetes version
KUBEBUILDER_ASSETS=$(setup-envtest use 1.29.0 -p path) \
go test -tags integration ./controllers/...
Best Practices¶
- Test happy path and error cases
- Use table-driven tests for multiple scenarios
- Keep tests focused - one thing per test
- Clean up resources in AfterEach/teardown
- Use meaningful assertions with clear messages
- Avoid sleeps - use Eventually/Consistently from Gomega
- Test status conditions not just resource existence