Writing V2 Tests
Follow these invariants when adding or migrating v2 E2E tests. Existing tests that violate them are migration debt, not examples to copy.
Choose the test boundary
Tests consume pre-created, fully operational hosted clusters. Provisioning, initial readiness, and teardown belong to lifecycle orchestration. Do not add startup polling to specs or suite setup to compensate for incomplete provisioning.
Non-lifecycle tests only inspect existing state. Skip when a required platform or configuration precondition is absent; never mutate the cluster to manufacture it. API failures must fail the test rather than become precondition skips.
Lifecycle tests may mutate state and wait for transitions they initiate. Label
them lifecycle so that read-only runs can exclude them. Capture original state
before mutation and register restoration. Register DeferCleanup immediately
after creating a resource, even when the scenario later deletes it explicitly.
Cleanup must tolerate resources already being absent while surfacing other errors.
If a resource already exists, reconcile it to the required state or fail with a
diagnostic; silently accepting it can invalidate the test baseline.
Structure coverage
Put feature-scoped specs in test/e2e/v2/tests/ and shared utilities in util/.
Use the registration pattern in
control_plane_workloads_test.go:
register specs during Ginkgo tree construction and obtain the runtime test
context inside hooks or specs. Do not register specs inside BeforeEach or It.
The suite supplies a shared
TestContext.
Fetch the HostedCluster and assert the returned error before testing platform or
configuration preconditions:
BeforeEach(func() {
tc := getTestCtx()
hc, err := tc.GetHostedCluster()
Expect(err).NotTo(HaveOccurred())
if hc.Spec.Platform.Type != hyperv1.AWSPlatform {
Skip("test requires AWS")
}
})
Re-fetch objects when checking mutations; do not assert against stale state.
Tests iterating over control plane workloads belong in
control_plane_workloads_test.go, with separate specs per workload so failures
identify the affected component. Add new workloads to the manually maintained
workload_registry.go
to extend existing compliance coverage, using its platform filtering where needed.
Assert the intended behavior
Assert current state directly. Poll only for asynchronous behavior or transitions
the test initiates. Choose timeouts and polling intervals appropriate to the
operation. Use tc.Context for API calls and polling so both use the context
provided by the framework.
Assertion helpers must preserve the caller's failure handling. A direct call
uses Gomega's Default; inside a polling callback, pass that callback's Gomega
instance through every helper. Nested callbacks use their own instance. Global
assertions inside polling callbacks bypass retry handling and may prematurely
fail or skip an informing spec. This applies to setup and cleanup helpers that
assert internally, too. See
Gomega's helper guidance.
For example, an assertion helper fetches current state and uses the supplied Gomega instance:
func expectReplicas(g Gomega, tc *internal.TestContext, key crclient.ObjectKey, want int32) {
GinkgoHelper()
np := &hyperv1.NodePool{}
g.Expect(tc.MgmtClient.Get(tc.Context, key, np)).To(Succeed())
g.Expect(np.Status.Replicas).To(Equal(want))
}
Inside a spec, choose a direct check or polling according to the behavior under test:
// Check state that should already hold.
expectReplicas(Default, tc, key, want)
// Wait for convergence after a lifecycle test changes replicas.
Eventually(func(g Gomega) {
expectReplicas(g, tc, key, want)
}).WithContext(tc.Context).
WithTimeout(5*time.Minute).WithPolling(10*time.Second).Should(Succeed())
Derive expected values from configuration when testing relationships between resources. Assert the outcome the scenario promises: an updated desired release alone does not establish that an upgrade completed. Identify affected resources and failed expectations in diagnostics.
Establish that the intended subjects were actually checked. Empty selections, unmatched searches, or early returns must not silently produce a passing test when they bypass the behavior it claims to verify.
Select tests and preserve their meaning
All new tests, including migrated tests, start informing. Apply
internal.InformingLabel to the new spec or inherit it from a container containing
only informing tests. When extending an existing blocking container, label the
new coverage without changing existing tests. Promotion to blocking is a separate,
deliberate change. The migration procedure defines the exception
for transferring existing blocking responsibility atomically.
Feature labels select coverage; the informing label controls failure handling.
Use labels on Describe and It; label Context only with explicit selection
intent, including informing status. Choose feature identities that represent
distinct capabilities and check existing names before adding one. Preserve test
names after Sippy import because renaming loses history and requires remapping.
Informing assertion failures are reported without failing the process; genuine
precondition skips remain skipped. A successful exit therefore does not mean all
assertions passed. Use --e2e.junit-report for the suite's custom reporting.
See the reporting contract
for the downstream format.
Supply prerequisites and CI selection
Reuse an existing cluster variant when its configuration and state are compatible with the test. A mutating test does not automatically need its own cluster; isolate it when its prerequisites or effects cannot be shared safely.
Define cluster variants explicitly, keeping configuration differences visible.
Variant names are a contract with external plans: the catalog must be fixed at
compile time and must not change with runtime conditions. Resolve runtime values
separately where practical. Put provisioning and day-2 mutations in lifecycle
hooks, and selection and ordering in TestPlan/TestMatrix. Consult
platform.go
for interfaces and hook timing.
A test runs wherever a plan's filter selects its labels. Update filters in the HyperShift repository when adding coverage that existing filters do not select. For job infrastructure changes, see CI Pipeline Configuration.
Groups targeting the same cluster must share one sequential lane; separate lanes run concurrently. A failed step prevents later steps in its lane from running. Ensure the plan selects the intended configurations and post-mutation checks.
Register test-suite environment variables in
env_vars.go
before reading them. Use built-in help for the current variables.
Verify and debug
Run against an already provisioned hosted cluster:
export KUBECONFIG=/path/to/management-cluster-kubeconfig
export E2E_HOSTED_CLUSTER_NAME=my-cluster
export E2E_HOSTED_CLUSTER_NAMESPACE=clusters
make e2ev2
E2E_SHOW_ENV_HELP=1 bin/test-e2e-v2
bin/test-e2e-v2 --ginkgo.label-filter="hosted-cluster-health" \
--ginkgo.v --e2e.junit-report=/tmp/junit.xml
Use --ginkgo.focus to narrow execution by description. Inspect actual selection,
skips, assertion results, and cleanup; a successful exit or skipped path is not
evidence that the intended behavior ran.
In CI, find the group's JUnit report in ARTIFACT_DIR. Use the plan and
run-tests log to identify its cluster. Check verbose Ginkgo output for the
assertion and source location, and distinguish informing failures from genuine
skips.
For provisioning failures, inspect the create-guests log and rollout JUnit
conditions. Use dump-guests logs to locate events and pod logs or diagnose
missing artifacts. Successful teardown does not establish that collection
succeeded.
Discuss framework changes first
Before submitting changes beyond adding test cases, discuss the proposal with
#forum-ocp-hypershift and agree on the approach. This includes platform
interfaces, lifecycle orchestration, shared helpers, selection, and reporting.
Explain why existing facilities are insufficient, consider alternatives and
cross-platform effects, and reference the agreement in the PR.
If no established pattern fits a new test area, consult
#wg-hypershift-e2e-v2 before establishing a convention and update this guide
after agreement.