advanced25 min studyLesson 360 in Engineering

Controller tests: attack the lifecycle boundaries

Build invariant-based scenarios for stale reads, conflicts, retries, deletion races, and stale leaders using the right test environment.

On this page

What you’ll understand

  • Separate safety assertions from eventual-progress assertions.
  • Choose unit, API integration, and cluster tests for the guarantees they cover.
  • Construct adversarial schedules around uncertain side effects and object incarnations.

A controller can pass a create-and-delete demonstration while still leaking external resources, overwriting concurrent fields, or exposing stale-leader effects. Correctness testing should target the boundaries where observations and accepted actions separate. Start with named safety invariants and explicit conditions for progress, then generate or script interleavings that challenge them. The goal is evidence about the real contract, not a test that merely repeats the implementation’s own branch structure.

Assert what must never happen and what must eventually happen

A safety invariant might require at most one external allocation per object UID, preservation of another actor’s label, or no removal of a finalizer before confirmed cleanup. A liveness assertion might require eventual readiness once the API, dependencies, and capacity stay healthy and intent stops changing. Those assumptions matter: no test should demand successful external deletion while the external service rejects every request forever. Conversely, a quick timeout is not proof that a controller can never make progress; it can be weak evidence from an unsuitable observation window.

Build a controllable external fake that records operation identities and permits a request to commit remotely while its response times out. Crash or restart the controller before its local success record, then reconcile again. Assert the external cardinality and returned identity rather than only the number of HTTP calls. Multiple calls can be correct when they implement an idempotent ensure. Also test delete-and-recreate with the same namespace/name but different UID so a stale action cannot silently target the new object’s resource.

∀t, Nexternal(UID,t)≤1\forall t,\ N_{\rm external}(UID,t)\leq1
This is an example safety requirement for a product that promises at most one external allocation per object incarnation. N counts allocations at time t. It must be supported by the chosen external identity/idempotency protocol; not every controller has this exact invariant.↔ Scroll wide equations horizontally.

A fake client cannot certify API-server semantics

Pure unit tests can check the desired action calculation and error classification quickly. An API-server integration environment is needed for server validation, resourceVersion conflicts, subresource behavior, and installed admission configuration. The Kubebuilder envtest documentation explains that its ordinary test control plane does not run built-in controllers. Therefore owner references do not automatically trigger garbage collection there. A test expecting a dependent to disappear needs a real cluster or the relevant controller, while an envtest assertion can verify the reference metadata your code creates.

Keep asynchronous tests condition-based: repeatedly observe the required state within a declared deadline instead of using one arbitrary sleep and assuming success. Capture useful logs, object versions, UID, and operation history on failure. Use deterministic scheduling controls where possible to reproduce a race, then add stress tests for broader schedules. Passing a hundred runs of one easy schedule is not equivalent to covering a single paused-leader overlap that the system’s safety property actually depends on.

healthy stable conditions⁡⇒◊desired invariant⁡\operatorname{healthy\ stable\ conditions}\Rightarrow\Diamond\operatorname{desired\ invariant}
The diamond denotes eventual satisfaction under explicitly stated stable conditions, including successful relevant operations and useful scheduling. This liveness specification has no universal finite time bound; a test adds an empirical deadline for its controlled environment.↔ Scroll wide equations horizontally.

Test the cross-product that your product actually needs

Create a table of failure locations: before finalizer persistence, after external commit but before response, after status conflict, during deletion, during watch expiration, and after leadership changes. Combine each with the relevant restart or duplicate notification rather than every imaginable combination. Include blocked permissions, invalid specs, and permanent external failures so the controller reports actionable state without hammering dependencies. Observe no-op steady state to catch status feedback loops that a single successful reconciliation would miss.

The optimization lab illustrates one narrow repeatability check. Reset, set η=1 and x₀=2, predict x₁=0, then Step once and again. The selected quadratic reaches its fixed point and remains there. Reset and set η=2.5, x₀=2 to predict first position −3 and an unstable trajectory as Step continues; out-of-range reporting is part of that model. It is not a controller test harness. The contrast helps explain why a test should exercise both a converging condition and a documented failing condition, then assert the actual system’s own invariant.

A timeout retry needs an external-state assertion

A controller uses one stable external key derived from UID u. A test service creates the resource on the first request but loses the response. After controller restart, two more ensure calls use the same key and return the existing resource.

  1. There are three external calls in total, but only one logical external resource should exist. Assert cardinality one and the same stable returned identity, not an incorrect requirement that only one call ever occur.

  2. Add an unrelated label update between an API read and status write, forcing a conflict. The controller must reread and preserve that update while recovering; a fixed token substituted into stale contents would fail the preservation assertion.

  3. Finally request deletion, confirm the external target is absent, and verify only the controller’s owned finalizer is removed. These three boundaries test uncertain success, concurrency, and cleanup rather than merely replaying one successful create branch.

The result

The evidence concerns one resource despite three calls, preserved concurrent state, and cleanup justified by confirmation. It does not require a fictitious exactly-once network.

Explore the model

Interactive lab

Interactive lab · optimization

Check both a fixed point and an unstable condition

Reset, set η=1 and x₀=2, predict x₁=0, then Step twice to see it remain zero. Reset and set η=2.5 and x₀=2; predict x₁=−3, then Step. This quadratic descent instrument is not a controller test harness or convergence proof for reconciliation.

Check both a fixed point and an unstable conditionGradient descent on f(x) = x²/2: xₙ = (1 − η)ⁿx₀. Playback advances two discrete iterations per second. Values outside |x| ≤ 4 are explicitly reported. Current readings: Position x0: 2; Objective x²/2: 2; Iteration behavior: Stable step. All parameters are adjustable with the labeled controls below.−4−202402.174.356.528.7Parameter xf(x) = x²/2n = 0
ObjectiveDiscrete iterate
Position x0
2
Objective x²/2
2
Iteration behavior
Stable step
n = 0 · t = 0 s

Gradient descent on f(x) = x²/2: xₙ = (1 − η)ⁿx₀. Playback advances two discrete iterations per second. Values outside |x| ≤ 4 are explicitly reported. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.

Look forA meaningful test states the expected behavior under both successful and adversarial conditions.

Check your understanding

An envtest-based test creates a valid owner reference but no built-in garbage collector is running. What should the test assume?

Try it yourself

A controlled retry test sends four ensure calls with one supported idempotency key and observes one external resource. Is four calls a correctness failure? Name a separate stale-leader test.

Take a moment to make a prediction or work it out on paper. Open a hint when you need one.

Give me a hint

Judge the resource invariant and API contract, then construct overlap rather than just counting elections.

Compare with the solution

Four calls can be correct when the receiving API’s ensure/idempotency semantics leave exactly one intended resource. Separately pause an old leader before an external effect, let a new leader install stronger authority, then resume the old request and verify the target’s documented fencing or safety behavior. Lease acquisition alone is not the assertion.

Take it with you

  • Safety, liveness, timing, and no-op behavior require different assertions.
  • Choose a test environment that actually contains the mechanism being verified.
  • Adversarial schedules should target identity, conflict, side-effect, deletion, and leadership boundaries.

Read further

Original sources and deeper explanations to follow at your own pace.

Optional progress, saved in this browser when available. Revisit any lesson at any time.

Lesson not marked complete.