Testing¶
This page describes CAPTF’s test suite: what make test runs, its tiers, the end-to-end test environment, coverage floors, golden files, and running less than the whole suite.
Running the tests¶
make test runs go test -race -count=1 ./... in every module (., api and test). That is the unit tier, and nothing it runs creates a Kubernetes cluster, calls a real cloud API, or invokes terraform or tofu.
- Controllers and webhooks run against the controller-runtime fake client, never a real API server; there is no envtest in this repository.
- Job execution is faked the same way: a reconciler test asserts on the
batch/v1.JobCAPTF would create, and a runner test drives the runner’s own logic directly, without a pod ever starting. internal/state,internal/outputsandinternal/locksinstead read realkubernetes-backend state:internal/state/testdata/fixturesholds state Secrets and lock Leases captured once from real Terraform and OpenTofu runs, checked in as frozen data. Regenerating them needs a capture setup that does not exist in this repository; treat those files as read-only.go test ./templates/decodes every shippedtemplates/*.yamlobject strictly into its API type and checks the valueshack/verify-templates.shrenders; it also runs CAPI’s ownClusterClassadmission webhook and topology generator, applying the class patches, againstclusterclass-noop.yaml.- The packages under
test/framework(kind, images, providers, waits, diagnostics) are unit-tested with fakes. They never start a process or touchpodman.
Test tiers¶
| Tier | What | How it runs |
|---|---|---|
| Unit | Everything above, including reconcilers driven through several packages at once against the fake client, such as internal/controllers/shared’s TestBringUpJobCount, which reconciles a whole cluster bring-up and asserts the resulting Job count. | make test, make test-cover and CI, always. |
| E2E | test/e2e/... and test/env/lifecycle, against a real kind cluster on podman or docker. | Only through the testenv-* and e2e-* targets, or go test -tags=e2e with -run. CI does not run them yet. |
Three guards keep the tiers apart:
- The build tag. Every Go file under
test/e2e/andtest/env/lifecycle/starts with//go:build e2e, sogo test ./...never compiles it.make verify-test-tiers(part ofmake verify, and of CI) fails if one lacks the tag, or if any other Go file uses it.hack/verify-test-tiers_test.shtests that script against fixtures; it is not part ofmake test, andverify-test-tiersruns it first. -run. Each e2e package’sTestMainrefuses to run unless-runselects a test, so a barego test -tags=e2e ./...does nothing.- Compiled without running.
make vetandmake lintadd a-tags e2epass over thetestmodule, so tagged code cannot rot unnoticed.
Besides go test, two make verify checks exercise the release assets without a real cloud: make verify-templates renders templates/ with the pinned clusterctl, and make verify-local-repository runs clusterctl generate provider and generate cluster against a local repository of the release assets, offline. hack/check-metadata_test.sh and hack/version_test.sh test hack/check-metadata.sh and hack/version.sh against fixtures under hack/testdata; make verify-metadata and make verify-version run them before the check itself.
End-to-end tests¶
The e2e tier needs podman or docker, and network access on the first run to download the pinned kind, Cluster API and cert-manager assets. Every version and image is pinned in test/framework/versions.go.
The test environment¶
make testenv-up builds the manager image from your working tree and brings up a kind cluster with cert-manager, Cluster API (core, kubeadm bootstrap and kubeadm control plane) and CAPTF. It takes about 6 minutes cold and about 15 seconds when it reuses a cluster. Work in a loop:
make testenv-up
source bin/testenv/captf-test-dev/env.sh
kubectl get pods -A
make testenv-reload
make testenv-logs
make testenv-down
env.sh points KUBECONFIG at the test cluster. testenv-reload rebuilds the image and rolls the manager over to it; testenv-logs writes a diagnostics bundle without Secrets; testenv-down deletes the cluster and keeps the artifacts. Everything for a cluster lives in bin/testenv/<name>/. Make Targets lists the targets and their variables.
The environment is built so it cannot touch anything else on your host:
- Cluster names must start with
captf-test-, andtestenv-downonly ever deletes such clusters. - The nodes join a dedicated
captf-testnetwork, not kind’s default one. - It never reads
~/.kube/config; every child process gets the cluster’s own kubeconfig. - It never changes host sysctls or limits, and it never deletes a cluster implicitly:
testenv-uprefuses an existing one unlessCAPTF_TESTENV_REUSE=1.
The e2e suites¶
| Target | Suite | What it proves |
|---|---|---|
make e2e-foundation | TestFoundation in test/e2e/foundation | In six ordered stages, a failed one stopping the run: the captf-test-e2e cluster builds, the base components and the CAPTF install are healthy, a real reconcile of a TerraformClusterIdentity works and the cluster holds still for a stability window, then a green light is written. |
make e2e-noop | TestNoop in test/e2e/noop | The published no-op modules, driven through real Cluster API objects, move data end to end with no cloud: the CAPI spec into the module inputs, the outputs into CAPTF status and on into CAPI, the cluster’s exports into the machine and pool inputs, the pinned digests into every later Job, and deletion into a full cleanup. |
make e2e-down | Deletes the e2e cluster; the artifacts are kept. |
Run e2e-foundation first. e2e-noop fails at once, and never skips, unless the foundation suite’s green light is still valid: the cluster exists, its pins and manager image match this build, every stage passed, and the light is younger than CAPTF_E2E_GREENLIGHT_MAX_AGE (24 hours by default). The foundation suite keeps a passing cluster for later tests unless CAPTF_E2E_TEARDOWN=1, and on failure it keeps the cluster too and writes a diagnostics bundle into bin/testenv/<name>/artifacts/. Each e2e-noop run uses a fresh namespace e2e-noop-<suffix> and removes what it created. Set CAPTF_E2E_NOOP_BAD_DIGEST=1 to run its negative check instead, in which one machine gets a nonexistent image digest and the suite must fail on the image pull.
test/README.md in the provider repository has the full stage-by-stage description, the variables and the allowlists of known-benign log lines.
Coverage¶
make test-cover runs the unit tests with -race and -covermode=atomic through gotestsum, and writes a profile and a JUnit report per module into bin/. make cover-check then reads the profiles and fails when a package falls below its floor in hack/coverage-floors.txt (the checker is hack/covercheck). CI runs both and writes the per-package table to the job summary.
The floors are a ratchet, set per package:
- A
defaultline sets the floor for every package without its own line, currently 80 percent. - A
<package> <percent>line sets one package’s floor. Packages are import paths relative to the module path. - A
<package> exemptline reports a package but never gates it. Each entry carries its reason in a comment; thecmd/*packages are exempt because the logic they call lives in theirapppackages.
When a package’s coverage rises, raise its floor in the same change. Never lower a floor or add an exemption without a reason on the line.
Golden files¶
Several packages pin an exact rendered output as a checked-in file and compare against it on every run, rather than asserting field by field:
| Package | What it pins |
|---|---|
internal/contract | The rendered JSON of fully populated contract inputs. |
internal/jobs | The batch/v1.Job of every operation, as YAML. |
internal/outputs | Rendered output fixtures. |
internal/render | The generated root module. |
cmd/tfcapi-lint | Its --json output. |
Run the affected test with UPDATE_SNAPSHOTS=1 to rewrite its golden files. make verify-schemas also validates the golden inputs and outputs of internal/contract and internal/outputs against the contract schemas.
Read the diff before committing a rewrite
Read the diff before committing it: a passing rewrite is not the same as a correct one.
Running one package, or one test¶
go test -race ./internal/jobs/...
go test -race -run TestGoldenJobs ./internal/jobs/...
UPDATE_SNAPSHOTS=1 go test ./internal/jobs/... -run TestGoldenJobs
go.work makes the api module’s tests reachable from the repository root too: go test ./api/... needs no cd. The test module’s unit tests run from its own directory: cd test && go test ./framework/....