Skip to content

Run tests with Kubernetes Jobs

Module authors can write end-to-end tests as Kubernetes Jobs that are run by Timoni, after the app workloads are deployed on a cluster.

After the app workloads are installed and become ready, Timoni will apply the Kubernetes Jobs and will wait for the created pods to run to completion. On upgrades, Timoni will delete the previous test pods and will recreate the Jobs for the current module values and version.


Test runs are idempotent, if the module version and values don't change, Timoni will not create new test pods. Tests are run only when a drift is detected in desired state.


To demonstrate how tests work, we'll add a Kubernetes Job that verifies that the app is accessible from inside the cluster, by running a curl command against the app's service.

Test definition

Add the following CUE definition to the module's templates directory:

package templates

import (

    corev1 ""
    batchv1 ""
    timoniv1 ""

#TestJob: batchv1.#Job & {
    #config:    #Config
    apiVersion: "batch/v1"
    kind:       "Job"
    metadata: timoniv1.#MetaComponent & {
        #Meta:      #config.metadata
        #Component: "test"
    metadata: annotations: timoniv1.Action.Force
    spec: batchv1.#JobSpec & {
        template: corev1.#PodTemplateSpec & {
            let checksum = uuid.SHA1(uuid.ns.DNS, yaml.Marshal(#config))
            metadata: annotations: "": checksum
            spec: {
                containers: [{
                    name:            "curl"
                    image:           #config.test.image.reference
                    imagePullPolicy: #config.test.image.pullPolicy
                    command: [
                restartPolicy: "Never"
        backoffLimit: 1

We set the force annotation on the Job's metadata to instruct Timoni to recreate the Job when it's spec changes. And we compute the checksum of the module's values that we set as an annotation on the Job's pod template. This will trigger a Job recreation when the module version or values change.

Test configuration

To allows users to enable testing, we'll add a test section to the module's #Config definition:

#Config: {
    test: {
        enabled: *false | bool
        image: timoniv1.#Image & {
            repository: *"" | string
            tag:        *"latest" | string
            digest:     *"" | string

In the module's #Instance definition we'll add the #TestJob to the tests list:

#Instance: {
    config: #Config

    tests: curl: #TestJob & {#config: config}

And finally we'll configure Timoni to apply the tests objects in the timoni.cue file with:

timoni: {

    apply: app: [for obj in instance.objects {obj}]

    // Conditionally run tests after an install or upgrade.
    if instance.config.test.enabled {
        apply: test: [for obj in instance.tests {obj}]

Note that the apply: test section goes last, after the apply: app section. Timoni executes the apply sections in order, so the app workloads will be deployed first and then the tests will be applied.

Test execution

To verify that the test configuration works, enable testing in the module's debug_values.cue file:

values: {
    test: enabled: true

Running the vet command with the debug should print the test Job:

timoni mod vet --debug --name myapp
INF vetting with debug values
INF Deployment/default/myapp valid resource
INF Service/default/myapp valid resource
INF Job/default/myapp-test valid resource
INF valid image (digest missing)
INF valid image (digest missing)
INF valid module

Running the apply command with the debug values should create the test Job, after the app workloads are deployed.