| name | test-standards |
| description | Apply when writing, modifying, or reviewing test files (_test.go). Covers test level selection, assertion quality, BDD naming, and use of shared helpers. |
When writing tests, follow these standards. When reviewing tests, flag violations and suggest fixes with code examples.
Choosing the right test level
Pick the cheapest level that can adequately cover the behaviour. Do not escalate without reason.
Unit test (no client or fake client, no envtest) — for pure logic, conversions, single-function behaviour, error paths that don't depend on server-side behaviour. Use fake.NewClientBuilder() only when the test doesn't depend on realistic API server responses (field defaulting, status subresource semantics, conflict errors, SSA merge, etc.). These run in milliseconds.
Integration test (envtest) — for anything that interacts with a Kubernetes API: controller reconciliation loops, multi-resource interactions, watching, status updates, and any scenario where fake client behaviour diverges from a real API server. Prefer envtest over fakes when in doubt — faking accurately is hard and flaky fakes waste more time than the slower test. Use pkg/test.StartEnvTest() in suite_test.go. These run in seconds.
E2E test (e2e/) — only for behaviour that requires real infrastructure: actual machine provisioning, cloud API interactions, cross-component migration flows. These run in minutes.
Rules of thumb:
- If you're testing "does this function return the right value/error" and it doesn't need a client → unit test.
- If you're testing any controller or client interaction → integration test (envtest).
- If you're testing "does a real machine appear in the cloud" → e2e test.
- If envtest can reproduce the scenario, do not write an e2e test.
Use existing shared helpers
Before writing new test utilities, builders, matchers, or setup code, search the repo for existing ones — particularly in pkg/test/, pkg/conversion/test/, pkg/admissionpolicy/testutils/, e2e/framework/, and the vendored testutils/resourcebuilder/ package. Do not duplicate what already exists. If you need a variant, extend the existing helper rather than creating a parallel one.
Debuggable failures
Every test failure must be debuggable from the output alone — without reading test source code. This means two things:
1. Assertion messages. If a failure's stack trace and default matcher output wouldn't tell you what went wrong, add a description. This applies especially to generic matchers like BeNil(), BeTrue(), HaveLen() where the default output doesn't convey intent.
Expect(transport).To(BeNil(), "expected nil transport when additionalTrustedCA is not set")
Expect(transport).To(BeNil())
2. Stack traces. Do not call Expect, Fail, or panic from helper functions — failures will point at the helper, not the test that called it. Return errors to the calling test instead.
If assertions inside a helper are unavoidable, use GinkgoHelper() so the stack trace shows the caller:
func expectResourceReady(obj client.Object) {
GinkgoHelper()
Expect(obj.GetAnnotations()).To(HaveKey("ready"))
}
BDD test names
Describe expected behaviour, not implementation details. Names should read as specifications.
It("should reject machines with duplicate provider IDs", func() { ... })
It("should return an error from validateProviderID", func() { ... })
Use nested Context blocks with when to set up preconditions:
Context("when the infrastructure cluster is not ready", func() {
It("should requeue after 30 seconds", func() { ... })
})
Use By() to document distinct phases within a test.
Concise assertions
Prefer precise matchers over multiple loose ones. Combine into one Expect when possible.
Expect(err).To(MatchError(expectedErr))
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(ContainSubstring("connection refused")))
When an expected error is reused across multiple test cases, declare it as a variable rather than duplicating the literal:
expectedErr := fmt.Errorf("connection refused")
Expect(err).To(MatchError(expectedErr))
Expect(err).To(MatchError("connection refused"))
Combine related assertions into a single matcher (e.g., SatisfyAll, ConsistOf). With Eventually, each separate assertion polls with its own timeout — multiple assertions checking the same object multiply the wait time on failure.
No sleeps, no timeout bumps
In event-driven systems, tests should wait for conditions, not for time to pass.
- Never use
time.Sleep(). Use Eventually with a condition that checks the actual state you're waiting for.
- Do not bump
Eventually timeouts to fix flaky tests. A flaky test means the test is waiting for the wrong condition or the code has a race. Fix the root cause.
Consistently durations should be meaningful. Too-short durations prove nothing — the condition might change immediately after. Use a duration long enough to cover at least a few reconciliation cycles.
Eventually(komega.Object(machine)).Should(HaveField("Status.Phase", Equal("Running")))
time.Sleep(5 * time.Second)
Expect(machine.Status.Phase).To(Equal("Running"))
General
- Follow patterns from CLAUDE.md (Komega, DescribeTable, resource builders, testutils cleanup).
- Remove
FIt/FContext before committing.