diff --git a/docs/getstarted/builders-workshop/3-testing.md b/docs/getstarted/builders-workshop/3-testing.md
index 59412a1b..e599d752 100644
--- a/docs/getstarted/builders-workshop/3-testing.md
+++ b/docs/getstarted/builders-workshop/3-testing.md
@@ -46,8 +46,8 @@ This command:
* Generates the basic structure for writing tests
:::note
-The default testing language is KCL. You can specify `--langauage=python` in
-the test command
+The default testing language is KCL. You can also specify `--language=python` or
+`--language=go` to generate tests in Python or Go.
:::
## Set up your test imports
diff --git a/docs/manuals/cli/howtos/testing.md b/docs/manuals/cli/howtos/testing.md
index 31db0d81..c096a1dd 100644
--- a/docs/manuals/cli/howtos/testing.md
+++ b/docs/manuals/cli/howtos/testing.md
@@ -17,6 +17,7 @@ To test your compositions and run end-to-end tests, make sure you have:
* The `up` CLI `v0.38` or higher [installed][installed]
* An Upbound account
+* Authenticated with Upbound using `up login`
## Development control planes
@@ -36,7 +37,7 @@ Preview how your composition creates resources with the `up composition render`
command before you deploy to a development control plane.
```shell
-up composition render apis/xbuckets/composition.yaml examples/bucket/example-xr.yaml
+up composition render apis/xstoragebuckets/composition.yaml examples/xstoragebuckets/example-xr.yaml
```
This command requires a **Composite Resource** (XR) file that defines the
@@ -79,44 +80,108 @@ generate` command.
### Generate a composition test
Composition tests validate the logic of your compositions without requiring a
-live environment. They simulate the composition controller's behavior, allowing
+live environment. They simulate the composition function pipeline, allowing
you to test resource creation, dependencies, and state transitions with mock
data.
You can generate tests with `up test generate` for composition tests.
-You can write tests in KCL or Python.
+You can write tests in KCL, Python, or Go.
For example, to generate a composition test:
+
+```shell {copy-lines="all"}
+up test generate my-test --language=go
+```
+
+
-```ini {copy-lines="all"}
-up test generate --language=python
+```shell {copy-lines="all"}
+up test generate my-test --language=python
```
-```ini {copy-lines="all"}
-up test generate --language=kcl
+```shell {copy-lines="all"}
+up test generate my-test --language=kcl
```
#### Author a composition test
-Composition tests use a declarative API in KCL or Python. Each test
-models a single composition controller loop, making testing more streamlined for
+Composition tests use a declarative API in KCL, Python, or Go. Each test
+models a single function pipeline run, making testing more streamlined for
reading and debugging.
-This testing command simulates the Crossplane composition controller. The
-controller evaluates the current state of resources, processes the composition,
-and makes necessary changes. The command recreates this process locally to
-verify composition logic.
+The test runner invokes your composition functions with a given XR input and
+compares the composed resource output against `assertResources`. It doesn't
+exercise the Crossplane composition controller, which handles reconciliation and
+external resource management.
+
+
+```go
+// Package main generates a CompositionTest
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/yaml"
+
+ metav1 "dev.upbound.io/models/io/k8s/meta/v1"
+ metav1alpha1 "dev.upbound.io/models/io/upbound/dev/meta/v1alpha1"
+)
+
+func main() {
+ assertResources := resourcesToItems[metav1alpha1.CompositionTestSpecAssertResourcesItem]()
+ test := metav1alpha1.CompositionTest{
+ APIVersion: ptr.To(metav1alpha1.CompositionTestAPIVersionmetaDevUpboundIoV1Alpha1),
+ Kind: ptr.To(metav1alpha1.CompositionTestKindCompositionTest),
+ Metadata: &metav1.ObjectMeta{
+ Name: ptr.To("test-xstoragebucket-default-go"),
+ },
+ Spec: &metav1alpha1.CompositionTestSpec{
+ AssertResources: &assertResources,
+ CompositionPath: ptr.To("apis/xstoragebuckets/composition.yaml"),
+ XrPath: ptr.To("examples/xstoragebuckets/example.yaml"),
+ XrdPath: ptr.To("apis/xstoragebuckets/definition.yaml"),
+ TimeoutSeconds: ptr.To(120),
+ Validate: ptr.To(false),
+ },
+ }
+ output := map[string]interface{}{
+ "items": []interface{}{test},
+ }
+ out, err := yaml.Marshal(output)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error encoding YAML: %v\n", err)
+ os.Exit(1)
+ }
+ fmt.Print(string(out))
+}
+```
+
+:::note
+`up test generate` produces the complete `main.go`, including helper functions
+`resourcesToItems`, `toItem`, and `convertViaJSON`. The snippet above shows only
+`main()`. Do not copy it as a standalone file — run `up test generate` first,
+then edit the generated file.
+:::
+
+Import your provider resource types from `dev.upbound.io/models` and pass them
+as arguments to `resourcesToItems` to populate `assertResources`. The test
+runner calls `go run .` and captures the YAML printed to stdout.
+
+
```python
@@ -140,7 +205,7 @@ test = compositiontest.CompositionTest(
-```shell
+```kcl
import models.io.upbound.dev.meta.v1alpha1 as metav1alpha1
_items = [
@@ -172,16 +237,22 @@ everything in that directory.
up test run tests/*
```
-To run a specific test, give the full path of that test:
+To run a specific test, give the path to that test directory:
+
+```shell
+up test run tests/my-test
+```
+
+For KCL tests you can also point to the specific file:
```shell
-up test run tests/xstoragebucket-default/main.k
+up test run tests/my-test/main.k
```
You can provide wildcards to run tests matching a pattern:
```shell
-up test run tests/xstoragebucket/**/*.k
+up test run tests/xstoragebucket/**
```
The command returns a summary of results:
@@ -197,12 +268,13 @@ up test run tests/*
SUCCESS Failed tests: 0
```
-When you run Compositions tests, Upbound:
+When you run composition tests, Upbound:
-1. Detects the test language and converts to a unified format.
-2. Builds and pushes the project to local daemon.
-3. Sets the context to the new control plane.
-4. Executes tests and validates results.
+1. Detects the test language from the files present (`main.k`, `main.py`, or `go.mod`).
+2. For Go tests, runs `go run .` locally and captures the YAML output.
+3. Builds composition functions and pushes them to the local Docker daemon.
+4. Sets the context to the local control plane.
+5. Executes tests and validates results.
### Generate an end-to-end test
@@ -210,22 +282,28 @@ End-to-end tests validate compositions in real environments, ensuring creation,
deletion, and operations work as expected.
You can generate test with `up test generate` for end-to-end tests.
-You can write tests in KCL or Python.
+You can write tests in KCL, Python, or Go.
-For example, to generate a end-to-end test:
+For example, to generate an end-to-end test:
+
+```shell {copy-lines="all"}
+up test generate my-e2e-test --e2e --language=go
+```
+
+
-```ini {copy-lines="all"}
-up test generate --e2e --language=python
+```shell {copy-lines="all"}
+up test generate my-e2e-test --e2e --language=python
```
-```ini {copy-lines="all"}
-up test generate --e2e --language=kcl
+```shell {copy-lines="all"}
+up test generate my-e2e-test --e2e --language=kcl
```
@@ -233,11 +311,74 @@ up test generate --e2e --language=kcl
#### Author an end-to-end test
-End-to-end tests use the `E2ETest` API, written in KCL or Python.
+End-to-end tests use the `E2ETest` API, written in KCL, Python, or Go.
+
+
+```go
+// Package main generates an E2ETest
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/yaml"
+
+ metav1 "dev.upbound.io/models/io/k8s/meta/v1"
+ metav1alpha1 "dev.upbound.io/models/io/upbound/dev/meta/v1alpha1"
+)
+
+func main() {
+ manifests := resourcesToItems[metav1alpha1.E2ETestSpecManifestsItem]()
+ extraResources := resourcesToItems[metav1alpha1.E2ETestSpecExtraResourcesItem]()
+ test := metav1alpha1.E2ETest{
+ APIVersion: ptr.To(metav1alpha1.E2ETestAPIVersionmetaDevUpboundIoV1Alpha1),
+ Kind: ptr.To(metav1alpha1.E2ETestKindE2ETest),
+ Metadata: &metav1.ObjectMeta{
+ Name: ptr.To("e2etest-xstoragebucket-go"),
+ },
+ Spec: &metav1alpha1.E2ETestSpec{
+ Crossplane: &metav1alpha1.E2ETestSpecCrossplane{
+ AutoUpgrade: &metav1alpha1.E2ETestSpecCrossplaneAutoUpgrade{
+ Channel: ptr.To(metav1alpha1.E2ETestSpecCrossplaneAutoUpgradeChannelRapid),
+ },
+ },
+ DefaultConditions: &[]string{"Ready"},
+ Manifests: &manifests,
+ ExtraResources: &extraResources,
+ SkipDelete: ptr.To(false),
+ TimeoutSeconds: ptr.To(4500),
+ },
+ }
+ output := map[string]interface{}{
+ "items": []interface{}{test},
+ }
+ out, err := yaml.Marshal(output)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error encoding YAML: %v\n", err)
+ os.Exit(1)
+ }
+ fmt.Print(string(out))
+}
+```
+
+:::note
+`up test generate` produces the complete `main.go` including helper functions.
+The snippet above shows only `main()` — run `up test generate` first, then edit
+the generated file.
+:::
+
+Populate `manifests` with your claim or XR resources and `extraResources` with
+any prerequisites such as `ProviderConfig`. Import your resource types from
+`dev.upbound.io/models` and pass them to `resourcesToItems`.
+
+
+
```python
@@ -298,7 +439,7 @@ test = e2etest.E2ETest(
-```shell
+```kcl
import models.com.example.platform.v1alpha1 as platformv1alpha1
import models.io.upbound.aws.v1beta1 as awsv1beta1
import models.io.upbound.dev.meta.v1alpha1 as metav1alpha1
@@ -352,16 +493,16 @@ everything in that directory.
up test run --e2e tests/*
```
-To run a specific test, give the full path of that test:
+To run a specific test, give the path to that test directory:
```shell
-up test run --e2e tests/e2etest-xstoragebucket-default/main.k
+up test run --e2e tests/my-e2e-test
```
You can provide wildcards to run tests matching a pattern:
```shell
-up test run --e2e tests/xstoragebucket/**/*.k
+up test run --e2e tests/xstoragebucket/**
```
The command returns a summary of results:
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 784dc8be..3a6333d5 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -12,6 +12,7 @@ const config = {
organizationName: "upbound",
projectName: "docs",
onBrokenLinks: "warn",
+ onBrokenMarkdownLinks: "warn",
scripts: [
{
src: "https://cdn-cookieyes.com/client_data/401fea7900d8d7b84b9e7b40/script.js",
@@ -43,9 +44,6 @@ const config = {
},
markdown: {
mermaid: true,
- hooks: {
- onBrokenMarkdownLinks: "warn",
- },
},
themes: ["@docusaurus/theme-mermaid"],
presets: [
diff --git a/package-lock.json b/package-lock.json
index 883c2d30..dceaa927 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
"@upbound/elements": "0.0.1",
"@upbound/utils": "0.0.1",
"@upbound/ux": "0.0.2",
+ "clean": "^4.0.2",
"clsx": "^2.1.1",
"docusaurus-pushfeedback": "^1.0.5",
"js-yaml": "^4.1.0",
@@ -8991,6 +8992,12 @@
"astring": "bin/astring"
}
},
+ "node_modules/async": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
+ "integrity": "sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==",
+ "license": "MIT"
+ },
"node_modules/autoprefixer": {
"version": "10.4.23",
"funding": [
@@ -9671,6 +9678,18 @@
"version": "5.0.5",
"license": "MIT"
},
+ "node_modules/clean": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/clean/-/clean-4.0.2.tgz",
+ "integrity": "sha512-2LGVh4dNtI16L4UzqDHO6Hbl74YjG1vWvEUU78dgLO4kuyqJZFMNMPBx+EGtYKTFb14e24p+gWXgkabqxc1EUw==",
+ "license": "MIT",
+ "dependencies": {
+ "async": "^0.9.0",
+ "minimist": "^1.1.0",
+ "mix2": "^1.0.0",
+ "skema": "^1.0.0"
+ }
+ },
"node_modules/clean-css": {
"version": "5.3.3",
"license": "MIT",
@@ -13965,6 +13984,15 @@
"sourcemap-codec": "^1.4.8"
}
},
+ "node_modules/make-array": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/make-array/-/make-array-0.1.2.tgz",
+ "integrity": "sha512-bcFmxgZ+OTaMYJp/w6eifElKTcfum7Gi5H7vQ8KzAf9X6swdxkVuilCaG3ZjXr/qJsQT4JJ2Rq9SDYScWEdu9Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/markdown-extensions": {
"version": "2.0.0",
"license": "MIT",
@@ -20033,6 +20061,17 @@
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"license": "MIT"
},
+ "node_modules/skema": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/skema/-/skema-1.0.2.tgz",
+ "integrity": "sha512-5LWfF2RSW2B3xfOaY6j49X8aNwsnj9cRVrM5QMF7it+cZvpv5ufiOUT13ps2U52sIbAzs11bdRP6mi5qyg75VQ==",
+ "license": "MIT",
+ "dependencies": {
+ "async": "^0.9.0",
+ "make-array": "^0.1.2",
+ "mix2": "^1.0.0"
+ }
+ },
"node_modules/skin-tone": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz",
diff --git a/package.json b/package.json
index 6a2cde8b..9c5763c2 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"@upbound/elements": "0.0.1",
"@upbound/utils": "0.0.1",
"@upbound/ux": "0.0.2",
+ "clean": "^4.0.2",
"clsx": "^2.1.1",
"docusaurus-pushfeedback": "^1.0.5",
"js-yaml": "^4.1.0",