Kubernetes — Testing#
Sampling note: Kubernetes is an XL project (~9,573 non-test Go files, 3,014 test files). Testing analysis used targeted grep across the full repository (excluding vendor/) for counts and representative examples. Deep-reads were focused on:
pkg/controller/deployment/(unit tests),test/integration/deployment/andtest/integration/secrets/(integration tests),test/e2e/framework,staging/src/k8s.io/client-go/kubernetes/fake/(generated fakes), andpkg/controller/testutil/(test helpers).
Test metrics#
- Test files: 3,014 (
*_test.gofiles, excl. vendor) - Non-test source files: 9,573
- Ratio (test / source): ~0.315
- Test frameworks:
- stdlib
testing— the primary framework for all unit and integration tests - Ginkgo v2 + Gomega — used exclusively in
test/e2e/andtest/e2e_node/(6,650+ginkgo.usages in e2e) github.com/google/go-cmp— 2,211 uses ofcmp.Diff/cmp.Equalas the comparison engine for assertion failures (preferred overreflect.DeepEqual)klog/v2/ktesting— 3,051 uses; provides atesting.T-backedkloglogger so structured log output is captured per-testgithub.com/stretchr/testify— peripheral use; appears in some staging packages (apimachinery,cluster-bootstrap) but is explicitly avoided in core packages (seeapitesting/close.gocomment: “assertNoError simulates assert.NoError without adding testify as a non-test dependency”)
- stdlib
Test organization#
Placement#
Tests are split across three tiers, each with its own package conventions:
Unit tests —
*_test.gofiles co-located with source, typically in a separatepackage foo_test(external black-box) or occasionally inpackage foo(white-box). Both conventions coexist without a uniform rule.Integration tests —
test/integration/(388 Go files, 40+ subdirectories organized by feature area:deployment,apiserver,auth,dra,scheduler, etc.). These use stdlibtestingdirectly, not Ginkgo.End-to-end tests —
test/e2e/(553 Go files) andtest/e2e_node/(137 Go files, linux-only build tag). Both use Ginkgo/Gomega exclusively.
Helper packages#
pkg/controller/testutil/— controller-specific helpers:UpdatePodStatus,CreateFakePod, action-inspecting utilities for the fake client tracker. Used across most controller unit tests.staging/src/k8s.io/client-go/testing/— the fake object tracker (fixture.go). Implements an in-memory REST store that intercepts CRUD calls and dispatches them to registeredReactFnchains. This is the foundational layer for all fake client testing.staging/src/k8s.io/client-go/kubernetes/fake/— code-generated (client-gen) fake clientset implementingclientset.Interface. Every resource type gets a typed fake (e.g.,fakeappsv1.FakeDeployments) that delegates to the shared object tracker.cmd/kube-apiserver/app/testing/andstaging/src/k8s.io/apiserver/pkg/testing/—StartTestServerOrDiehelpers that spin up a real API server in-process for integration tests.test/e2e/framework/— a large framework (~553 Go files collectively) with test lifecycle helpers, cluster clients, event watchers, polling utilities, and thegomega.Expect-based assertion patterns.
Fixtures#
test/fixtures/— YAML fixtures forkubectland CLI tests (doc-yaml examples).testdata/directories scattered throughout staging (e.g.,code-generator/cmd/validation-gen/output_tests/*/testdata) for golden-file testing of code generators.- No global fixtures database; each package owns its fixture data.
Test patterns#
Table-driven tests#
Prevalence: Extremely heavy — 7,246 occurrences of
testCases,tests :=, ort.Run(tc.namein*_test.gofiles. This is the single most uniformly applied coding convention in the entire project.Style: Anonymous struct slices with a mandatory
name stringfield. Subtests always uset.Run(tc.name, ...).// pkg/controller/deployment/deployment_controller_test.go (representative) tests := []struct { name string d *apps.Deployment rsList []*apps.ReplicaSet podList *v1.PodList wantErr bool expected int32 }{ { name: "no replica sets", d: newDeployment("d", 3, nil, nil, nil, nil), expected: 0, }, // ... } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // exercise }) }Assessment: The pattern is not enforced by linting tools but is so culturally ingrained that deviations are effectively absent in reviewed code. Every regression bug fix adds a new table row, making test files serve as living specifications. This is the gold standard of table-driven testing in the Go ecosystem.
Mocking approach#
Strategy: Interface-backed generated fakes, not framework-generated mocks. The project does not use gomock or mockery.
The fake client pattern:
clientset.Interface ←── real generated client (HTTPS to API server) ↑ fake.NewClientset() ←── generated fake (in-memory object tracker)fake.NewClientset(objects...)initializes an object tracker pre-populated with seed objects. Tests inject the fake clientset into the component under test via its constructor. API calls from the component are intercepted, recorded astesting.Actionvalues, and can be inspected:// pkg/controller/deployment/deployment_controller_test.go client := fake.NewSimpleClientset() // ... run controller with client ... actions := client.Actions() // assert specific verbs/resources were calledReactFn chains: Tests can register custom reactor functions (
client.AddReactor("get", "pods", ...)) to inject faults or override specific behaviors, enabling precise failure scenario testing.Manual fakes for domain types: Components like the
SharedInformerFactory,EventRecorder, and scheduler framework plugins provide hand-written fakes (e.g.,record.NewFakeRecorder(100)) rather than generated ones.No gomock: The avoidance of interface mocking frameworks is intentional — the project relies on real fake implementations of its own interfaces rather than expecting test authors to set up call expectations.
Integration tests#
Present: Yes — a dedicated
test/integration/tree with 388 Go files spanning 40+ functional areas.How: Integration tests spin up a real API server in-process plus a real etcd instance using
StartTestServerOrDie:// test/integration/secrets/secrets_test.go server := kubeapiservertesting.StartTestServerOrDie( t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd(), // reuses a shared in-process etcd ) defer server.TearDownFn() client := clientset.NewForConfigOrDie(server.ClientConfig)The API server runs as a goroutine in the same process, not in Docker.
SharedEtcd()manages a single etcd instance shared across tests in the same binary, improving speed.t.Cleanup/defer TearDownFn()ensures teardown.Separation: No build tags separate integration from unit tests — they live in a completely different directory tree (
test/integration/vs co-located*_test.go). CI scripts select them by path. This is simpler than build tags but means you cannot accidentally co-locate integration tests with unit tests.Scope: Integration tests exercise full API server request handling (admission, validation, storage round-trips via etcd, watch propagation) against actual components, not fakes.
E2e tests#
- Framework: Ginkgo v2 + Gomega, with a large custom framework (
test/e2e/framework/). - Pattern: BDD-style
Describe/Itblocks organized by feature area (apps, auth, network, dra, etc.). - Target: A running Kubernetes cluster. E2e tests do not spin up the control plane; they receive
--kubeconfigas input and exercise live resources. - Node e2e:
test/e2e_node/targets a single node runningkubeletdirectly; marked//go:build linux. - Separation: E2e tests are isolated by directory convention and by the Ginkgo runner (
hack/ginkgo-e2e.sh), not by build tags.
Test quality observations#
What’s done well#
Table-driven tests as cultural norm: The consistency is exceptional. 7,246+ instances means almost every
TestFoofunction uses the pattern. New contributors learn it immediately, and test additions are always predictable in structure.Generated fakes are comprehensive:
client-genproduces fake clients for every API group and version. Tests never need to mock networking. The object tracker (testing/fixture.go) faithfully implements strategic merge patch, JSON patch, and optimistic locking, making fakes a high-fidelity stand-in for the real server.In-process integration testing: Spinning up a real API server + etcd in-process (rather than Docker containers or external services) is fast, hermetic, and gives near-production fidelity. It catches admission webhook bugs, etcd encoding issues, and watch event fan-out problems that fake clients would miss.
go-cmpfor diffs: Usingcmp.Diffoverreflect.DeepEqualgives human-readable failure messages showing exactly which field diverged. The 2,211 usages represent a deliberate, project-wide preference.ktestingintegration: Capturing structuredklogoutput per-test (klog/v2/ktesting) means controller logs appear in test output only for failing tests — no global log pollution.Three-tier test pyramid respected: Unit (fast, isolated, fakes) → Integration (in-process API server) → E2e (live cluster). Each tier has a clear scope and toolchain.
What could improve#
testify avoidance creates verbose assertions: The explicit avoidance of
testifymeans test assertion code is more verbose (if !cmp.Equal(...) { t.Errorf(...) }vs.require.Equal(t, ...)).go-cmppartially compensates, but control-flow assertions (require.NoError) still require hand-rolledif err != nil { t.Fatalf(...) }.Inconsistent package placement: Some tests use
package foo, others usepackage foo_test, without a clear rule. This creates occasional import cycles and makes it harder to know which internal state is accessible in tests.E2e framework size: The
test/e2e/framework/package has grown very large. Without reading it exhaustively, it shows signs of accumulation (bugs.go, flake_reporting_util.go) that suggest it could benefit from decomposition.No fuzz testing infrastructure visible: With Go’s native
testing.Favailable since 1.18, Kubernetes-scale input parsing (YAML/JSON API objects) would benefit from fuzzing, but noFuzzXxxfunctions are present in sampled code.
Patterns worth emulating#
Table-driven tests with
t.Run: The most portable, readable, and extensible Go testing pattern. Kubernetes’s consistent application is the canonical example.In-process real-server integration testing: The
StartTestServerOrDie+SharedEtcdpattern proves that integration tests do not need Docker. An API server and storage backend can live in-process, keeping CI times manageable while exercising real codepaths.Generated fakes with a shared object tracker: The
fake.NewClientset()+ReactFnpattern decouples tests from networking entirely while preserving API semantics. Replicable for any project that exposes a client interface.go-cmpwithcmpopts: Usingcmp.Diffwith options (e.g.,cmpopts.IgnoreFields,cmpopts.EquateEmpty) gives precise, self-documenting assertion failures. Better thanreflect.DeepEqualfor complex nested structs.ktestingfor structured logging in tests: Integrating the project’s own logger withtesting.Tso per-test log output is captured and only shown on failure. Any project usingklog(orslog) should adopt this pattern.