author-tests
Use this skill when user requests to write, create, author, modify, refactor or plan refactoring of Crossplane configuration tests (composition tests or E2E tests) in a control-plane project - in any
SKILL.mdskills/author-tests/SKILL.md
GitHubCrossplane Test Authoring
Author and modify Crossplane configuration tests, in any language up test generate supports,
and write the assertion that has to fail first.
Before you start
A test's meaning is language-agnostic; only its syntax differs. Every test compiles to a
CompositionTest or an E2ETest (meta.dev.upbound.io/v1alpha1). The rules are in this file;
the object model, structuring patterns and common mistakes in
test-model.md (read it before your first test in a project); the
syntax in the charter's file for the test language (Phase 1).
- Writing or changing an
E2ETest: Phase 4. - Planning or executing a test refactor: refactoring.md (plan into
.agents/tasks/REFACTOR_TESTS.mdwithout executing, then one item per run). - Not here: building the package or running the whole suite as a gate, except Phase 6
(
verify-configuration); running E2E tests (e2e-test-configuration); implementing composition features (author-composition).
Binding rules — they hold even if you open nothing else
Load control-plane-project-charter first, or read its SKILL.md beside this skill's
directory: this skill does not load it, and the charter has the reasons. The project's own API,
design or gate script wins over these: say where you departed.
- Never block on a question nobody can answer. Interactive, ask only what the project can't tell you; unattended, never ask: decide from the spec and state the assumption, or stop and report (charter §1).
- Never create a group, Space, control plane or cloud resource as a side effect, and never
pass
--publicyourself (§9). Runup test run --e2eonly after loading e2e-test-configuration: it has each target's preconditions, and the read-back and teardown rules. - Test first: watch each new composition or unit test fail for the reason you intended;
for a new E2ETest this is optional. A syntax error, a missing path,
✗ Parsing tests, a compile error or a bug in the test's own logic is a broken test, not RED. Backfill by mutating the implementation, never the expected value (§3). - In a v2 project, managed resources carry
forProvideronly, on the.m.API groups, unless the project's spec or API sets more: nodeletionPolicy,managementPoliciesormetadata.namespace; omitproviderConfigRefif and only ifClusterProviderConfig/defaultexists and is the right one. One whose connection details the function reads also needswriteConnectionSecretToRef, and a missing detail never falls back to a value. A v1 project stays v1 (§5). - Claim only what ran: the command's own exit code, not
tail's: redirect, then read$?(cmd > /tmp/x.log 2>&1; echo "exit=$?"); after a pipe, bash${PIPESTATUS[0]}, zsh$pipestatus[1].No test files foundmeans nothing ran. Name the layer you reached — render, composition test, local control plane, cloud — and never claim one you did not reach; for each new test, the change that turns it red, or call it unproven. Comments and docs claim no more (§4, §8).
Phase 1: Detect the test language — do this first
Pick the language as charter §10 says (existing tests, else the composition language, else YAML; functions in more than one language and no tests yet: ask), then read its file before writing a test: it carries the templates and the silent failure modes.
Phase 2: Decide what to test, and scaffold
New test: determine the feature, resources and variants from
argsand the project (apis/*/definition.yaml,apis/*/composition.yaml, the function source, the example XRs inexamples/*/*.yaml) — do not ask. Modifying a test: read it, match its language and style, and understand its current assertions before you change them.Checking what is not a render.
up test runevaluatesCompositionTestandE2ETestobjects and nothing else. For the declarative files — dependencies inupbound.yaml, the XRD,examples/, aManagedResourceActivationPolicy— sort what can be checked:- What a render reaches, cover with a render. XRD defaults reach the render through
xrdPath(charter §2, §8), so assert the defaulted values on the composite. Render a shipped example withxrPath: examples/<kind>/<file>.yamlplusxrdPath: that proves the function handles it, not that the API server accepts it. A pipeline function missing fromdependsOnalready fails every render (unknown function). - The rest is outside this suite.
up project builddoes not validateexamples/, and it accepted an MRAP without its API dependency (up v0.55.0).<author-configuration-package>/scripts/check_xrd_schema.pychecks XRD design, not a frozen surface;<author-configuration-package>is that skill's directory, beside this one. Where the project's gate script already runs such checks, they stay there, beside the build and the test run (charter §2: the project's gate wins). - Write no test program or checker for these files, and don't copy a skill's script into the
project (charter §3: test-first is for behaviour). Invoke
check_xrd_schema.pyby its path in its skill's directory, with the project root as the working directory; the build and the composition tests that use the files do the rest. - Never turn a test program into a linter. A test dir that checks repo files, exits
non-zero on a mismatch and prints
items: []adds zero tests. Passing, it drops out of the count (alone:No test files found, exit 0); failing, it stops at✗ Parsing tests, which is a broken test, not RED. - What no check here reaches — XRD
requiredlists, enums and scope; the dependency set — stays uncovered unless the project's gate checks it (charter §4: say what you did not verify).
- What a render reaches, cover with a render. XRD defaults reach the render through
Organise:
Type Dir prefix Timeout validatePurpose Composition test-≥60s false(scaffold default)local render, no cloud E2E e2etest-sized to what you provision (e2e.md) n/a real cloud lifecycle Consolidate in one directory: the same resource type in different configs, feature on/off variants, 3–5 related scenarios. Separate directories: different resource types, complex sequential dependencies, and every E2E test.
up test runruns every matched dir's program, so the composition gate isup test run "tests/test-*"(charter §7).Scaffold with
up test generate <name> [--e2e] --language <lang>(kcl,python,yaml,go,go-templating); never create a test directory by hand. Pass the bare name: the CLI prependstest-/e2etest-. Then write the test from the template in the language file. Python: runsetup_venv.pyagain after generating each test directory (control-plane-project-charter/references/languages/python.md).
Phase 3: Write the assertion that has to fail
Four things make an assertion bite:
| Assert the field, not the existence. | assertResources is partial and positive. An entry naming only kind passes against any resource of that kind, whatever it contains. Name the field you are adding. |
| Assert on the composite too. | Every status field the function writes needs an assertion on the XR itself. It is the only programmatic check on composition outputs. |
| Cover the minimal XR. | Use the inline xr field with every optional property omitted. That is the shape a real user writes first, and the one the scaffold never generates. |
| Use distinguishing inputs. | Every parameter the function passes through (region, config names, CIDRs, the XR's own name, …) gets a non-default value, unique across fields, in at least one test; a required field with no default needs two tests with different values. An input equal to the default or to a sibling field can't tell pass-through from a hard-coded constant. Backfill check: control-plane-project-charter/references/charter/tdd.md. |
- Define the XR inline where the format supports it, with
namespace: default(v2). - Assert list membership on the parsed list, not by substring matching on a joined string:
a short token matches inside a longer one (
dbinsidedb-subnet-group). - Composed-resource names: never guess one; the naming rule is in
control-plane-project-charter/references/charter/evidence.md(test-model.md, mistake 1). - Managed resources in a test follow binding rule 4, written as the import path in KCL,
Python and Go and as the
apiVersionstring in YAML. AssertproviderConfigRefandmanagementPoliciesonly where the project's spec or API sets them (whether a render keeps a value equal to the model default depends on the language and SDK; see the language file).
Asserting absence
assertResources cannot assert absence: it has no absence operator, so do not search the CLI
for one.
| Must be absent | How |
|---|---|
| A composed resource | Assert the composite's spec.crossplane.resourceRefs as the exact list from the render, once per input shape, not in every case. Lists match exactly, so a surplus resource fails it. The "Composition test template" sections of control-plane-project-charter/references/languages/go/tests.md and control-plane-project-charter/references/languages/go-templating.md show this guard; detail in control-plane-project-charter/references/charter/evidence.md, "How assertResources matches" |
| A field | Not expressible in a composition test. Use a unit test on the function's desired state, in the function's own language (Go: go test ./... in functions/<n>/), or confirm it once in the render and report it as not asserted |
A Fatal result
A CompositionTest cannot assert a Fatal result. up test run renders through Crossplane, which
stops at the Fatal before any assertion: the test fails with
… returned a fatal result: <message> (observed with up v0.55.0), and a CompositionTest has no
field for an expected error, so not even a resourceRefs: [] guard runs. A Fatal case under
tests/test-* fails the gate. Test each fatal path in a function unit test (the message, nothing
composed; Go: control-plane-project-charter/references/languages/go/functions.md, "A path that
must return Fatal"), keep composition tests on inputs the function accepts, and don't search the
CLI, its binaries or the web for another way.
Asserting a property of every composed resource
When a property applies to every composed resource (a label, a policy, a config ref, a
region), assert it in one test that ranges over all desired resources, not in per-resource
expectations: those inherit each row's omissions, so a resource that misses it stays green. The
tier that can range is a function unit test over the desired state (Go sketch:
control-plane-project-charter/references/languages/go/functions.md, unit-test template). A
CompositionTest cannot iterate over the render; it fits only when one helper adds the property
to every expectation and there is one expectation per entry of the exact resourceRefs list.
Phase 4: E2E tests
Read e2e.md before writing or changing any E2ETest — fields and
defaults, credentials per target, the ProviderConfig the test creates, a Go template, and what
counts as an e2e RED.
defaultConditionslists condition types (Ready), never expressions or status paths.- An
E2ETestcannot assert a status field or a status condition: it waits only for thedefaultConditionstypes (Ready). To check a status value, use anobservedResourcesCompositionTest or a unit test; a value read back during an e2e run is report evidence, not an assertion (e2e.md). Do not search theupbinary or the web for another mechanism. - Set
timeoutSecondsexplicitly, sized to what you provision. Credentials depend on the target:source: Upboundworks only on a Spaces control plane. extraResourcescreates the ProviderConfig: by default aClusterProviderConfignameddefault, with no namespace (e2e.md).- Never put long-lived credentials in a test — use web identity, or a Secret filled from a
UP_*variable — and never setskipDelete: true: it leaves real cloud resources running. - Never print a generated
E2ETest, or a Secret, with real credential values, or write it to a file: itsextraResourcescarry the credential. Check it with a dummy value, and keep the warning in the template's header comment and fail message (e2e.md).
Phase 5: Run it — RED, then GREEN
Charter §3's loop, run directly with up test run "tests/<t>". If the run also changes the
function, author-composition Phase 4 says which skill runs RED, GREEN and the gate. Only when
you add or change tests alone are this phase and Phase 6 the whole loop.
Phase 6: The gate, after the loop
Once the suite is green:
- The project defines its own gate (a script or make target): run that, not
verify-configuration(charter §2: the project's own decisions win). - Otherwise hand off to
verify-configurationfor the build and the whole suite. - No control plane or deploy allowed (the project, the user or your instructions say so):
the gate is the build and the whole
up test run—verify-configurationPhases 1–2, or the project's gate — and nothing after it: no E2E, noup project run(binding rule 2). Where only Upbound Cloud is ruled out,verify-configuration's "Local-only projects and projects with their own gate" says what changes.
Success criteria
Checks for you before you report, not a report format (charter §4: report what ran). Test authoring is complete when:
- The test language was detected or chosen, and the scaffold generated (or the existing test read)
- The test follows this file, test-model.md and the matching language file
- The fields that matter are asserted, not just existence
- The new assertion was observed to fail before the implementation existed, and to pass after
- The gate passed once the suite was green — the project's own gate if it has one, else
verify-configuration, without a deploy where none is allowed (Phase 6)