Testing¶
This guide covers testing standards and practices for the Cloudflare Tunnel Gateway Controller.
Running Tests¶
Unit Tests¶
# Run all tests
go test -v ./...
# Run with race detector
go test -race ./...
# Run specific package
go test -v -race ./internal/controller/...
# Run specific test
go test -v -race ./internal/controller/... -run TestHTTPRouteReconciler
Coverage¶
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View coverage in browser
go tool cover -html=coverage.out
# View coverage in terminal
go tool cover -func=coverage.out
Helm Chart Tests¶
# Install helm-unittest plugin
helm plugin install https://github.com/helm-unittest/helm-unittest
# Run chart tests
helm unittest charts/cloudflare-tunnel-gateway-controller
# Lint chart
helm lint charts/cloudflare-tunnel-gateway-controller
# Template locally (for debugging)
helm template test charts/cloudflare-tunnel-gateway-controller \
--values charts/cloudflare-tunnel-gateway-controller/examples/basic-values.yaml
Test Patterns¶
Table-Driven Tests¶
Use table-driven tests with named test cases:
func TestFeature(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input InputType
expected OutputType
wantErr bool
}{
{
name: "valid input",
input: InputType{...},
expected: OutputType{...},
wantErr: false,
},
{
name: "invalid input",
input: InputType{...},
expected: OutputType{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result, err := DoSomething(tt.input)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
Parallel Execution¶
Always use t.Parallel() at test and subtest level:
func TestSomething(t *testing.T) {
t.Parallel() // Mark test as parallel
t.Run("subtest", func(t *testing.T) {
t.Parallel() // Mark subtest as parallel
// ...
})
}
Fake Client Setup¶
Use controller-runtime fake client for unit tests:
func TestController(t *testing.T) {
// Create scheme with all required types
scheme := runtime.NewScheme()
_ = clientgoscheme.AddToScheme(scheme)
_ = gatewayv1.Install(scheme)
// Create fake client with initial objects
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(
&gatewayv1.Gateway{...},
&gatewayv1.HTTPRoute{...},
).
Build()
// Create reconciler with fake client
reconciler := &HTTPRouteReconciler{
Client: client,
Scheme: scheme,
}
// Test reconciliation
result, err := reconciler.Reconcile(ctx, ctrl.Request{...})
require.NoError(t, err)
}
Test Libraries¶
| Library | Usage |
|---|---|
github.com/stretchr/testify/assert | Soft assertions (test continues) |
github.com/stretchr/testify/require | Hard assertions (test stops) |
sigs.k8s.io/controller-runtime/pkg/client/fake | Fake Kubernetes client |
sigs.k8s.io/controller-runtime/pkg/envtest | Integration tests |
Assert vs Require¶
// Use require for setup and critical checks (stops test on failure)
require.NoError(t, err, "setup should succeed")
// Use assert for multiple checks (test continues)
assert.Equal(t, expected.Name, actual.Name)
assert.Equal(t, expected.Port, actual.Port)
Test Organization¶
File Naming¶
| Pattern | Description |
|---|---|
*_test.go | Test files (standard Go convention) |
*_internal_test.go | Tests for unexported functions (same package) |
Test Helpers¶
Extract common setup into helper functions:
func setupFakeClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
scheme := runtime.NewScheme()
require.NoError(t, clientgoscheme.AddToScheme(scheme))
require.NoError(t, gatewayv1.Install(scheme))
return fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(objs...).
Build()
}
What to Test¶
Unit Tests¶
- Business logic functions
- Input validation
- Error handling paths
- Edge cases (empty inputs, nil values)
Integration Tests¶
- Controller reconciliation loops
- Kubernetes API interactions
- Cloudflare API interactions (mocked)
Not Tested¶
- Generated code (CRD types, mocks)
- Third-party library internals
Mocking¶
External Services¶
Mock external services (Cloudflare API) in unit tests:
type mockCloudflareClient struct {
tunnelConfig *cloudflare.TunnelConfiguration
err error
}
func (m *mockCloudflareClient) UpdateTunnelConfiguration(
ctx context.Context,
config cloudflare.TunnelConfiguration,
) error {
m.tunnelConfig = &config
return m.err
}
Time-Dependent Tests¶
Use injectable time for deterministic tests:
type Clock interface {
Now() time.Time
}
// In production
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
// In tests
type FakeClock struct {
CurrentTime time.Time
}
func (c FakeClock) Now() time.Time { return c.CurrentTime }
CI Integration¶
Tests run automatically in CI:
# .github/workflows/pr.yaml
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: coverage.out
Gateway API Conformance Tests¶
Conformance tests validate that the controller implements the Gateway API specification correctly. These tests require a real Kubernetes cluster with a working Cloudflare Tunnel.
Prerequisites¶
- Kubernetes cluster (kind, k3s, or real cluster)
- Controller installed and running
- Cloudflare Tunnel configured and working
- GatewayClass
cloudflare-tunnelcreated
Running E2E Tests¶
E2E tests run against a live kind cluster with Cloudflare Tunnel and L7 proxy deployed.
# Run all E2E tests
go test -v -race -tags e2e -count=1 -timeout=15m ./test/e2e/...
# Run a single test
go test -v -race -tags e2e -count=1 -timeout=15m ./test/e2e/... \
-run TestHTTPRouteSimpleSameNamespace
E2E Environment Variables¶
| Variable | Fallback | Default | Description |
|---|---|---|---|
E2E_TUNNEL_HOSTNAME | CONFORMANCE_TUNNEL_HOSTNAME | v2-test.lex.la | Tunnel hostname for requests |
E2E_KUBE_CONTEXT | CONFORMANCE_KUBE_CONTEXT | kind-v2-test | kubectl context |
E2E_NAMESPACE | CONFORMANCE_NAMESPACE | cloudflare-tunnel-system | Controller namespace |
E2E_TEST_NAMESPACE | CONFORMANCE_TEST_NAMESPACE | e2e-test | Test resources namespace |
E2E_GATEWAY_NAME | CONFORMANCE_GATEWAY_NAME | e2e-gateway | Gateway resource name |
E2E Test Coverage (24 tests)¶
Tests cover both Cloudflare Tunnel and L7 proxy features:
- Core (4): SimpleSameNamespace, PathPrefixMatching, ExactPathMatching, MatchingAcrossRoutes
- Extended (18): HeaderMatching, MethodMatching, QueryParamMatching, Weight, RequestHeaderModifier, ResponseHeaderModifier, RequestRedirect, RegexPathMatching, RegexHeaderMatching, RegexQueryParamMatching, PathMatchOrder, URLRewritePath, URLRewriteHost, RequestMirror, RedirectPort, RedirectPath, CombinedMatching, MultipleMatchesOR
- Gateway (2): AcceptedCondition, ObservedGenerationBump
Official Gateway API Conformance Suite¶
The project integrates the official sigs.k8s.io/gateway-api/conformance suite with a custom TunnelRoundTripper that routes requests through Cloudflare edge.
# Run conformance tests (requires deployed controller + tunnel)
go test -v -tags conformance -count=1 -timeout=30m ./test/conformance/...
# Generate conformance report
CONFORMANCE_REPORT_OUTPUT=./conformance-report.yaml \
go test -v -tags conformance -count=1 -timeout=30m ./test/conformance/...
| Variable | Default | Description |
|---|---|---|
CONFORMANCE_GATEWAY_CLASS | cloudflare-tunnel | GatewayClass name |
CONFORMANCE_REPORT_OUTPUT | (none) | Path for YAML conformance report |
CONTROLLER_VERSION | dev | Version for report metadata |
Profiles: GATEWAY-HTTP, GATEWAY-GRPC.
Best Practices¶
- Fast tests: Unit tests should run in milliseconds
- Isolated tests: No shared state between tests
- Deterministic tests: Same input = same output
- Readable tests: Test name describes behavior
- Minimal mocking: Only mock what's necessary
- Error testing: Test error paths, not just happy paths