This comprehensive guide will help you understand, run, and write tests for the ambient-code-backend using our Ginkgo-based test framework.
-
Go 1.24+: Ensure you have Go installed
go version # Should show 1.24 or higher -
Install Test Tools:
cd components/backend make install-tools # Installs Ginkgo CLI and other tools
-
Verify Setup:
ginkgo version # Should show Ginkgo v2.x.x
# Navigate to backend directory
cd components/backend
# Run all unit tests
make test-unit
# Or run with Ginkgo directly
ginkgo run --label-filter="unit" -vExpected output:
Running Suite: Ambient Code Backend Test Suite
===============================================
[unit, handlers, health] Health Handler
✓ Should return 200 OK with health status
[unit, handlers, middleware] Middleware Handlers
✓ Should accept valid Kubernetes namespace names
SUCCESS! -- 5 Passed | 0 Failed | 0 Pending | 0 Skipped
components/backend/
├── tests/ # Shared test framework utilities (not production code)
│ ├── config/ # Test configuration management
│ ├── logger/ # Test logging utilities
│ ├── test_utils/ # Reusable test utilities (HTTP/K8s fakes, token helpers)
├── handlers/ # Business logic + tests
│ ├── health.go
│ ├── health_test.go # ✅ Tests for health.go
│ ├── sessions.go
│ └── sessions_test.go # ✅ Tests for sessions.go
├── types/
│ ├── common.go
│ └── common_test.go # ✅ Tests for common.go
└── git/
├── operations.go
└── operations_test.go # ✅ Tests for operations.go
- Co-location: Tests live next to the code they test (easier to find and maintain)
- Shared Utilities: Common test logic in
tests/directory (avoid duplication) - Import Pattern: Test packages import utilities with
"ambient-code-backend/tests/..."
Our tests use labels for organization and filtering:
| Label | Purpose | Example Usage |
|---|---|---|
unit |
Pure unit tests, no external dependencies | Testing business logic |
integration |
Tests requiring real Kubernetes cluster | End-to-end workflows |
handlers |
HTTP handler tests | API endpoint testing |
types |
Type and utility function tests | Data structure validation |
git |
Git operation tests | Repository operations |
auth |
Authentication/authorization tests | Security testing |
slow |
Time-consuming tests | Performance tests |
# Run only handler tests
make test-handlers
# or
ginkgo run --label-filter="handlers" -v
# Run everything except slow tests
make test-fast
# or
ginkgo run --label-filter="!slow"
# Run auth tests only
make test-auth
# or
ginkgo run --label-filter="auth" -v
# Combine filters (unit tests for handlers, excluding slow ones)
ginkgo run --label-filter="unit && handlers && !slow" -vIntegration tests live under components/backend/tests/integration/ and are intended to run against a real Kubernetes/OpenShift cluster.
For local development authentication setup (since DISABLE_AUTH is not supported), see:
components/backend/README.md→ Local development authentication (DISABLE_AUTH removed)
cd components/backend
# Run Go integration tests directly (these are standard `go test` suites)
go test ./tests/integration/... -count=1
# If you are running a Ginkgo integration suite (labelled `integration`), use:
ginkgo run --label-filter="integration" -vCommon expectations for integration tests:
- You may need to set
TEST_NAMESPACEand ensure your kubeconfig points to a cluster you can modify. - Prefer cleaning up resources created during tests (namespaces, rolebindings, secrets).
- For RBAC validation, use
SetValidTestToken(...)with real Roles/RoleBindings created in the test namespace.
# Most common: Run unit tests with reports
make test-unit
# Run all tests (unit + integration)
make test-all
# Run with verbose output
make test-ginkgo-verbose
# Run tests in parallel (faster)
make test-ginkgo-parallel# Focus on specific test by name
make test-focus FOCUS="Should return 200 OK"
# Run with custom configuration
VERBOSE=true SKIP_SLOW_TESTS=true ginkgo run
# Run with timeout
ginkgo run --timeout=10m
# Generate coverage report
go test -cover ./handlers ./types ./gitConfigure test behavior with environment variables:
# Test execution
export VERBOSE="true" # Enable verbose logging
export SKIP_SLOW_TESTS="true" # Skip performance tests
export PARALLEL_NODES="4" # Run 4 tests in parallel
# Test environment
export TEST_NAMESPACE="my-test-ns" # Custom test namespace
export USE_REAL_CLUSTER="false" # Use fake K8s clients (default)
export CLEANUP_RESOURCES="true" # Clean up after tests
# Reporting
export ENABLE_REPORTING="true" # Generate test reports
export REPORTS_DIR="custom-reports" # Custom report directory
export LOGS_DIR="custom-logs" # Custom log directory
# Timeouts
export SUITE_TIMEOUT="30m" # Max time for entire test suite
export TEST_TIMEOUT="5m" # Max time per individual test
export API_TIMEOUT="30s" # Max time for API callsIf you're adding tests for components/backend/handlers/projects.go:
# Create the test file
touch components/backend/handlers/projects_test.gopackage handlers_test
import (
"net/http"
"ambient-code-backend/handlers"
"ambient-code-backend/tests/logger"
"ambient-code-backend/tests/test_utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Projects Handler", Label("unit", "handlers", "projects"), func() {
var (
httpUtils *test_utils.HTTPTestUtils
k8sUtils *test_utils.K8sTestUtils
)
BeforeEach(func() {
logger.Log("Setting up Projects Handler test")
httpUtils = test_utils.NewHTTPTestUtils()
k8sUtils = test_utils.NewK8sTestUtils(false, "test-namespace")
})
Context("When creating a project", func() {
It("Should create project successfully", func() {
// Arrange - Set up test data
projectRequest := map[string]interface{}{
"name": "test-project",
"description": "Test project description",
}
context := httpUtils.CreateTestGinContext("POST", "/api/projects", projectRequest)
httpUtils.SetAuthHeader("test-token")
httpUtils.SetUserContext("test-user", "Test User", "test@example.com")
// Act - Call the handler
handlers.CreateProject(context)
// Assert - Check the results
httpUtils.AssertHTTPStatus(http.StatusCreated)
var response map[string]interface{}
httpUtils.GetResponseJSON(&response)
Expect(response).To(HaveKey("name"))
Expect(response["name"]).To(Equal("test-project"))
logger.Log("Project created successfully: %s", response["name"])
})
It("Should reject invalid project names", func() {
// Test edge case - invalid input
projectRequest := map[string]interface{}{
"name": "Invalid Project Name!", // Invalid characters
}
context := httpUtils.CreateTestGinContext("POST", "/api/projects", projectRequest)
httpUtils.SetAuthHeader("test-token")
// Act
handlers.CreateProject(context)
// Assert
httpUtils.AssertHTTPStatus(http.StatusBadRequest)
httpUtils.AssertErrorMessage("Invalid project name")
})
})
})Context("When listing projects", func() {
BeforeEach(func() {
// Create test data for each test in this context
createTestProject("project-1", "test-namespace")
createTestProject("project-2", "test-namespace")
})
It("Should return all projects", func() {
context := httpUtils.CreateTestGinContext("GET", "/api/projects", nil)
httpUtils.SetAuthHeader("test-token")
handlers.ListProjects(context)
httpUtils.AssertHTTPStatus(http.StatusOK)
var response map[string]interface{}
httpUtils.GetResponseJSON(&response)
Expect(response).To(HaveKey("items"))
items := response["items"].([]interface{})
Expect(items).To(HaveLen(2))
})
It("Should support pagination", func() {
context := httpUtils.CreateTestGinContext("GET", "/api/projects?limit=1", nil)
httpUtils.SetAuthHeader("test-token")
handlers.ListProjects(context)
httpUtils.AssertHTTPStatus(http.StatusOK)
var response map[string]interface{}
httpUtils.GetResponseJSON(&response)
items := response["items"].([]interface{})
Expect(items).To(HaveLen(1))
Expect(response).To(HaveKey("hasMore"))
Expect(response["hasMore"]).To(BeTrue())
})
})// Helper function to create test projects
func createTestProject(name, namespace string) {
project := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": map[string]interface{}{
"name": name,
"labels": map[string]interface{}{
"test-framework": "ambient-code-backend",
},
},
},
}
k8sUtils := test_utils.NewK8sTestUtils(false, namespace)
k8sUtils.CreateCustomResource(context.Background(),
schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"},
"", project)
}It("Should handle POST request with JSON body", func() {
// Arrange
requestBody := map[string]interface{}{
"field1": "value1",
"field2": 123,
"nested": map[string]interface{}{
"key": "value",
},
}
context := httpUtils.CreateTestGinContext("POST", "/api/endpoint", requestBody)
httpUtils.SetAuthHeader("bearer-token")
httpUtils.SetProjectContext("my-project")
// Act
handlers.MyHandler(context)
// Assert
httpUtils.AssertHTTPStatus(http.StatusOK)
httpUtils.AssertJSONContains(map[string]interface{}{
"status": "success",
"id": BeNumerically(">", 0), // Using Gomega matcher
})
})It("Should require authentication", func() {
// No auth header set
context := httpUtils.CreateTestGinContext("GET", "/api/secure-endpoint", nil)
handlers.SecureHandler(context)
httpUtils.AssertHTTPStatus(http.StatusUnauthorized)
httpUtils.AssertErrorMessage("Authentication required")
})
It("Should accept valid token", func() {
context := httpUtils.CreateTestGinContext("GET", "/api/secure-endpoint", nil)
// For simple tests, arbitrary token is fine:
httpUtils.SetAuthHeader("valid-token")
handlers.SecureHandler(context)
httpUtils.AssertHTTPSuccess() // Any 2xx status
})
It("Should validate RBAC permissions", func() {
// Create a token with actual RBAC permissions
// This ensures tests match production RBAC behavior
context := httpUtils.CreateTestGinContext("GET", "/api/projects/test-project/agentic-sessions", nil)
// NOTE: create the namespace + Role needed for the test in BeforeEach
token, _, err := httpUtils.SetValidTestToken(
k8sUtils,
"test-project", // namespace
[]string{"get", "list"}, // verbs
"agenticsessions", // resource
"", // auto-generate SA name
"test-agenticsessions-read-role", // pre-created Role name
)
Expect(err).NotTo(HaveOccurred())
// Token is automatically set in Authorization header by SetValidTestToken
handlers.ListSessions(context)
httpUtils.AssertHTTPSuccess()
// This test verifies that the handler works with real RBAC permissions,
// not just arbitrary tokens that bypass security checks
})It("Should create Kubernetes resource", func() {
// Arrange
resource := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "vteam.ambient-code/v1alpha1",
"kind": "AgenticSession",
"metadata": map[string]interface{}{
"name": "test-session",
},
"spec": map[string]interface{}{
"initialPrompt": "Test prompt",
},
},
}
gvr := schema.GroupVersionResource{
Group: "vteam.ambient-code",
Version: "v1alpha1",
Resource: "agenticsessions",
}
// Act
created := k8sUtils.CreateCustomResource(ctx, gvr, "test-namespace", resource)
// Assert
Expect(created).NotTo(BeNil())
Expect(created.GetName()).To(Equal("test-session"))
// Verify it exists
k8sUtils.AssertResourceExists(ctx, gvr, "test-namespace", "test-session")
})Context("When handling errors", func() {
It("Should return 400 for invalid input", func() {
// Test with malformed JSON
context := httpUtils.CreateTestGinContext("POST", "/api/endpoint", "invalid-json")
handlers.MyHandler(context)
httpUtils.AssertHTTPStatus(http.StatusBadRequest)
})
It("Should return 404 for missing resource", func() {
context := httpUtils.CreateTestGinContext("GET", "/api/projects/nonexistent", nil)
// This sets an arbitrary token to satisfy handlers that require an auth header.
// It does NOT validate RBAC permissions. For RBAC tests, use SetValidTestToken.
httpUtils.SetAuthHeader("any-token")
handlers.GetProject(context)
httpUtils.AssertHTTPStatus(http.StatusNotFound)
httpUtils.AssertErrorMessage("Project not found")
})
})It("Should retry failed operations", func() {
attempt := 0
operation := func() error {
attempt++
if attempt < 3 {
return fmt.Errorf("simulated failure")
}
return nil
}
// Use test utility for retry logic
err := test_utils.RetryOperation(operation, 5, 100*time.Millisecond)
Expect(err).NotTo(HaveOccurred())
Expect(attempt).To(Equal(3))
})Important: For tests that need to validate RBAC permissions (matching production security model), use SetValidTestToken instead of SetAuthHeader with arbitrary tokens. This ensures tests use tokens that would work with real RBAC, not just strings that bypass security checks.
httpUtils := test_utils.NewHTTPTestUtils()
// Create contexts
context := httpUtils.CreateTestGinContext("GET", "/path", body)
// Set headers
httpUtils.SetAuthHeader("token") // Simple token (no RBAC validation)
// For tests that need RBAC validation, use SetValidTestToken:
token, saName, err := httpUtils.SetValidTestToken(
k8sUtils,
"namespace",
[]string{"get", "list", "create"},
"agenticsessions",
"", // optional SA name
"test-agenticsessions-write-role", // pre-created Role name
)
Expect(err).NotTo(HaveOccurred())
// Token is automatically set in Authorization header
httpUtils.SetUserContext("userID", "userName", "user@email.com")
httpUtils.SetProjectContext("projectName")
// Assertions
httpUtils.AssertHTTPStatus(200)
httpUtils.AssertHTTPSuccess() // Any 2xx
httpUtils.AssertHTTPError() // Any 4xx/5xx
httpUtils.AssertJSONContains(map[string]interface{}{"key": "value"})
httpUtils.AssertJSONStructure([]string{"id", "name", "status"})
httpUtils.AssertErrorMessage("Expected error message")
// Get responses
body := httpUtils.GetResponseBody()
var data MyStruct
httpUtils.GetResponseJSON(&data)k8sUtils := test_utils.NewK8sTestUtils(false, "namespace") // false = use fake clients
// Resource operations
created := k8sUtils.CreateCustomResource(ctx, gvr, namespace, resource)
resource, err := k8sUtils.GetCustomResource(ctx, gvr, namespace, name)
updated, err := k8sUtils.UpdateCustomResource(ctx, gvr, resource)
err := k8sUtils.DeleteCustomResource(ctx, gvr, namespace, name)
// Assertions
k8sUtils.AssertResourceExists(ctx, gvr, namespace, name)
k8sUtils.AssertResourceNotExists(ctx, gvr, namespace, name)
k8sUtils.AssertResourceHasStatus(ctx, gvr, namespace, name, map[string]interface{}{
"phase": "Running",
"ready": true,
})
// Secrets and ConfigMaps
secret := k8sUtils.CreateSecret(ctx, namespace, name, data)
configMap := k8sUtils.CreateConfigMap(ctx, namespace, name, data)
// Cleanup
k8sUtils.CleanupTestResources(ctx, namespace)// Random data generation
randomString := test_utils.GetRandomString(10)
testID := test_utils.GenerateTestID("test")
// Pointer helpers
stringPtr := test_utils.StringPtr("value")
intPtr := test_utils.IntPtr(42)
boolPtr := test_utils.BoolPtr(true)
// Operations
err := test_utils.RetryOperation(func() error {
return someOperation()
}, 3, time.Second)
// Logging
test_utils.WriteLogFile(specReport, "test-name", "logs/")# Run specific test by name
ginkgo run --focus="Should create project successfully"
# Run specific describe block
ginkgo run --focus="Projects Handler"
# Run tests matching pattern
ginkgo run --focus="create.*project"# Verbose output with test progress
ginkgo run -v
# Show stack traces on failure
ginkgo run --trace
# Keep going after first failure (default stops)
ginkgo run --keep-goingAfter running tests, check these locations:
# Test reports
ls reports/
# junit.xml - For CI integration
# results.json - Machine-readable results
# test_summary.txt - Human-readable summary
# Failure logs
ls logs/
# Contains detailed logs for failed tests
# Stack traces and captured outputAdd debug output to your tests:
It("Should debug issue", func() {
logger.Log("Debug: Starting test with value %v", testValue)
// Add intermediate assertions
Expect(preliminaryResult).NotTo(BeNil(), "Preliminary result should exist")
logger.Log("Debug: Preliminary result: %v", preliminaryResult)
// Use GinkgoWriter for output that appears in reports
GinkgoWriter.Printf("Debug info: %+v\n", complexObject)
// Final assertion
Expect(finalResult).To(Equal(expectedValue))
})When a test fails:
- Check the failure message: Shows expected vs actual values
- Review the logs: Look in
logs/directory for detailed output - Run with verbose:
ginkgo run -v --focus="failing test" - Add debug logging: Use
logger.Log()to trace execution - Isolate the test: Run just that one test to avoid interference
# Generate reports
ginkgo run --junit-report=reports/junit.xml --json-report=reports/results.json
# View coverage
go test -cover ./handlers ./types ./git
go test -coverprofile=coverage.out ./handlers ./types ./git
go tool cover -html=coverage.out -o coverage.html
open coverage.htmlname: Backend Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.24
- name: Install dependencies
run: |
cd components/backend
go mod download
make install-tools
- name: Run tests
run: |
cd components/backend
export ENABLE_REPORTING="true"
export SKIP_SLOW_TESTS="true"
make test-unit
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: components/backend/reports/
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: components/backend/coverage.out# Install Ginkgo CLI
go install github.com/onsi/ginkgo/v2/ginkgo@latest
# Or use make target
make install-tools# Update dependencies
go mod tidy
go mod download# Check for goroutine leaks
export GOMAXPROCS=1 # Limit concurrency for debugging
# Run with shorter timeout
ginkgo run --timeout=30s
# Add timeout to specific test
It("Should complete quickly", func(SpecContext) {
// Test will be cancelled after default timeout (5min)
}, SpecTimeout(30*time.Second))# Common cause: importing handler package from handler test
# Solution: Use separate _test package name
package handlers_test // Not: package handlers
import (
"ambient-code-backend/handlers" // Import the package under test
. "github.com/onsi/ginkgo/v2"
)# For integration tests, ensure proper RBAC
kubectl auth can-i create agenticsessions.vteam.ambient-code --namespace=test-namespace
# Check test namespace exists
kubectl get namespace test-namespace
# Reset test environment
make k8s-teardown
make k8s-setup- Review test logs: Check
logs/directory for detailed error information - Run with verbose output:
ginkgo run -vshows test progress - Review this guide: See
TEST_GUIDE.mdfor comprehensive testing documentation - Examine existing tests: Look at
handlers/*_test.gofor patterns - Ginkgo documentation: https://onsi.github.io/ginkgo/
- Gomega matchers: https://onsi.github.io/gomega/
If tests are running slowly:
# Run in parallel
ginkgo run -p
# Skip slow tests during development
export SKIP_SLOW_TESTS=true
make test-fast
# Profile test execution
ginkgo run --json-report=results.json
# Check results.json for test timingsBefore submitting your tests:
- Test file named correctly:
*_test.go - Package name: Use
package xyz_testpattern - Imports: Include required test utilities
- Labels: Add appropriate labels (
unit,handlers, etc.) - Descriptive names: Test descriptions explain what is being tested
- AAA pattern: Arrange, Act, Assert structure
- Edge cases: Test both success and failure scenarios
- Cleanup: Use
BeforeEach/AfterEachfor setup/teardown - No side effects: Tests don't affect each other
- Assertions: Use descriptive Gomega matchers
- Logging: Add
logger.Log()statements for debugging
When reviewing test code:
- Test coverage: Are all important code paths tested?
- Test quality: Do tests actually verify the intended behavior?
- Maintainability: Are tests easy to understand and modify?
- Performance: Are slow tests marked with
slowlabel? - Documentation: Are complex test scenarios explained?
- Consistency: Do tests follow established patterns?
You now have a comprehensive understanding of the ambient-code-backend test framework!
Quick reminder of the most important commands:
# Install tools and run tests
make install-tools
make test-unit
# Debug failing test
ginkgo run --focus="failing test name" -v
# Run specific category
make test-handlers
# Skip slow tests
make test-fastThe framework is designed to make testing easy and comprehensive. When in doubt, look at existing tests in handlers/*_test.go for patterns, and don't hesitate to add debug logging with logger.Log() to understand what's happening in your tests.
Happy testing! 🚀