← Files Testkube SkillsARCHIVED FILE

skills/testworkflow-author/references/step-patterns.md

13.7 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

# TestWorkflow Step Patterns

Common patterns for building TestWorkflow steps, from simplest to most complex.
Each pattern is a minimal, working example. Patterns marked **(self-contained)**
can be validated with `--dry-run` on their own.

## Pattern 1: Simple Shell (self-contained)

**Use when**: A single command in the default container image is all you need.

```yaml
apiVersion: testworkflows.testkube.io/v1
kind: TestWorkflow
metadata:
  name: simple-shell
spec:
  container:
    image: alpine:latest
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
  steps:
  - name: Run command
    shell: echo "Hello from Testkube" && uname -a
```

## Pattern 2: Run with Custom Image

**Use when**: You need a different container image or environment per step.
The `run` field gives you full control over the container for that step.

```yaml
steps:
- name: Lint Go code
  run:
    image: golang:1.26-bookworm
    shell: golangci-lint run ./...
    env:
    - name: GOFLAGS
      value: "-mod=readonly"
```

The step runs in the `golang:1.26-bookworm` image regardless of what
`spec.container.image` is set to. All other steps use the default image.

> **Note**: This pattern shows only the step portion. Place it under
> `spec.steps:` of a complete workflow with `apiVersion`, `kind`, `metadata`,
> and `spec.container` defined.

## Pattern 3: Inline Content (self-contained)

**Use when**: Your test script is short and doesn't need a Git repository.
Embed it directly in the workflow under `spec.content.files`.

```yaml
apiVersion: testworkflows.testkube.io/v1
kind: TestWorkflow
metadata:
  name: inline-content
spec:
  content:
    files:
    - path: /data/test.js
      content: |
        import http from 'k6/http';
        export default function() { http.get('https://test.k6.io'); }
  container:
    image: grafana/k6:0.49.0
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
    workingDir: /data
  steps:
  - name: Run inline test
    shell: k6 run test.js --duration 5s
```

Relative paths in `files` resolve against the container's working directory.
Use `/data/` prefix for absolute paths that always work.

## Pattern 4: Git Checkout

**Use when**: Your test code lives in a Git repository. Testkube handles
the clone — no `git clone` commands needed.

```yaml
spec:
  content:
    git:
      uri: https://github.com/kubeshop/testkube
      revision: main
      paths:
      - test/k6/k6-smoke-test.js
  container:
    workingDir: /data/repo/test/k6
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
  steps:
  - name: Run test from repo
    run:
      image: grafana/k6:0.49.0
      shell: k6 run k6-smoke-test.js --duration 5s
```

The repo is cloned to `/data/repo` by default. Set `mountPath` to change
this. Use `paths` for sparse checkout — only those directories are fetched.
For private repos, add `token`/`sshKey` or their `*From` secret references.

## Pattern 5: Artifact Collection (self-contained)

**Use when**: Your test produces output files (reports, logs, screenshots)
that you want to save and view after execution.

```yaml
apiVersion: testworkflows.testkube.io/v1
kind: TestWorkflow
metadata:
  name: artifact-collection
spec:
  container:
    image: alpine:latest
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
  steps:
  - name: Generate output
    shell: mkdir -p /data/artifacts && echo "test results" > /data/artifacts/report.txt
  - name: Save artifacts
    condition: always
    artifacts:
      paths:
      - /data/artifacts/**
```

Key rules:
- Artifact paths are relative to the step's `workingDir`. Use absolute paths
  (`/data/artifacts/**`) to avoid ambiguity.
- Use `condition: always` so artifacts are saved even if the test step fails.
- Glob patterns use [doublestar](https://github.com/bmatcuk/doublestar) syntax.

## Pattern 6: Template Usage

**Use when**: Reusing a predefined TestWorkflowTemplate with configuration
parameters. Templates encapsulate common patterns (official/k6, official/cypress).

```yaml
steps:
- name: Run k6 from template
  template:
    name: official/k6
    config:
      version: "0.49.0"
      duration: "30s"
```

The template's config parameters depend on the template definition.
Check the template's `spec.config` for available parameters. Templates are
inlined before execution — `use` at top-level shares defaults across the
entire workflow; `template` at step-level is fully isolated.

## Pattern 7: Execute Composition

**Use when**: Running multiple existing workflows or tests as a suite.
The `execute` step orchestrates other Testkube resources by name.

```yaml
steps:
- execute:
    parallelism: 2
    workflows:
    - name: playwright-smoke
    - name: cypress-smoke
```

Set `parallelism` to control how many execute concurrently. The workflows
referenced must already exist in the same namespace.

## Pattern 8: Parallel Execution

**Use when**: Running the same test with different parameters (matrix)
or splitting work across shards.

### Simple parallel (count)

Run N identical copies of a step. Use when you don't need per-copy variation.

```yaml
steps:
- name: Parallel k6
  parallel:
    count: 3
    run:
      image: grafana/k6:0.49.0
      shell: k6 run test.js --duration 30s
```

### Matrix parallel (production)

Run the same step with different configurations — browsers, versions, datasets.
This is the production pattern, commonly used with `transfer` and `fetch`.

```yaml
steps:
- name: Run tests across browsers
  parallel:
    # One worker per matrix value
    matrix:
      browser: ['chrome', 'firefox']
    # transfer: share files to workers before they start.
    # Without this, each worker starts with an empty filesystem.
    transfer:
    - from: /data/repo
    # fetch: collect files from workers after they complete.
    # Routes each worker's output to a browser-specific directory.
    fetch:
    - from: /data/artifacts
      to: /data/artifacts/{{ matrix.browser }}
    # Per-worker resources — override global defaults
    container:
      resources:
        requests:
          cpu: 1500m
          memory: 2Gi
    run:
      args:
      - '--browser'
      - '{{ matrix.browser }}'
  # Parent-level artifacts include everything fetched from workers
  artifacts:
    paths:
    - '**/*'
```

**transfer** copies files from the current step's container to all workers before
they start — essential for sharing a git checkout or pre-built assets. **fetch**
copies files from workers back to the parent after they complete — use it to
collect per-worker results without losing them.

Matrix values are accessible via `{{ matrix.<name> }}` in descriptions, command
args, env vars, and artifact paths. For numeric iteration, use `count` and
access `{{ index }}` and `{{ count }}` in each worker.

For a complete working example, see `examples/parallel-matrix.yaml`.

Add `failFast: true` to cancel remaining workers on first failure:

```yaml
parallel:
  failFast: true
  matrix:
    browser: ['chrome', 'firefox']
```

Control max concurrent workers with `parallelism` (defaults to `count`):

```yaml
parallel:
  count: 10
  parallelism: 3  # only 3 workers run at a time
```

## Pattern 9: Services (Sidecars)

**Use when**: Your test needs a companion service — a database, Selenium
browser, Docker daemon, or mock API. Services run in separate containers
alongside your workflow steps.

```yaml
spec:
  services:
    postgres:
      image: postgres:16-alpine
      timeout: 120s
      logs: always
      env:
      - name: POSTGRES_USER
        value: test
      - name: POSTGRES_PASSWORD
        value: test
      - name: POSTGRES_DB
        value: testdb
      readinessProbe:
        tcpSocket:
          port: 5432
        periodSeconds: 1
  steps:
  - name: Run tests with DB
    run:
      image: node:22
      shell: |
        echo "DB host: {{ services.postgres.0.ip }}"
        npm test
      env:
      - name: DATABASE_URL
        value: postgres://test:test@{{ services.postgres.0.ip }}:5432/testdb
```

Reference a service by name and index: `{{ services.postgres.0.ip }}` gives
the IP of the first postgres service instance. Use `readinessProbe` to ensure
the service is ready before steps execute.

## Pattern 10: Config + Expressions (self-contained)

**Use when**: You want the workflow to accept parameters — version numbers,
URLs, counts — that can be overridden at runtime via CLI or the dashboard.

```yaml
apiVersion: testworkflows.testkube.io/v1
kind: TestWorkflow
metadata:
  name: config-expressions
spec:
  config:
    targetUrl:
      type: string
      default: "https://test.k6.io"
    VUS:
      type: integer
      default: 5
  content:
    files:
    - path: /data/test.js
      content: |
        import http from 'k6/http';
        export default function() { http.get(__ENV.TARGET_URL); }
  container:
    image: grafana/k6:0.49.0
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
    workingDir: /data
  steps:
  - name: Run parameterized test
    shell: k6 run -e TARGET_URL={{ config.targetUrl }} --vus {{ config.VUS }} test.js
    artifacts:
      paths:
      - '**/*'
```

Override config at runtime: `testkube run testworkflow config-expressions --config VUS=10 --config targetUrl=https://example.com`

Available expression helpers: `{{ config.param }}`, `{{ services.name.0.ip }}`,
`{{ shellquote(env.VAR) }}`, `{{ env.VAR }}`.

### Secret references via expressions

Use `secret()` to reference Kubernetes Secrets in env vars without exposing values:

```yaml
env:
- name: API_TOKEN
  value: '{{ secret("my-secret", "token") }}'
```

### Math in expressions

Arithmetic works directly in `{{ }}` blocks:

```yaml
shell: k6 run --vus {{ config.workers * 5 }} test.js
```

Expressions also support comparison (`config.VUS > 10 ? 5 : 2`), string
functions (`split()`, `join()`), and JSON parsing. Full reference:
https://docs.testkube.io/articles/test-workflows-expressions

### Credential references

Use `credential()` for Testkube-managed credentials (managed in Dashboard):

```yaml
env:
- name: API_KEY
  value: '{{ credential("my-api-key") }}'
```

### Global environment variables

Standard `env` entries only reach the main container. Add `global: true` to
propagate to services and parallel workers:

```yaml
container:
  env:
  - name: TARGET_URL
    value: "https://staging.example.com"
    global: true
```

## Pattern 11: Cron Trigger (self-contained)

**Use when**: The workflow should run on a schedule — hourly, daily, weekly.
Uses standard Cron syntax.

```yaml
apiVersion: testworkflows.testkube.io/v1
kind: TestWorkflow
metadata:
  name: cron-trigger
spec:
  events:
  - cronjob:
      cron: "0 * * * *"
      # timezone is optional — defaults to UTC if not set
      timezone: "Europe/Warsaw"
  container:
    image: alpine:latest
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
  steps:
  - name: Scheduled task
    shell: echo "Running at $(date)"
```

Common cron expressions:

| Expression | Meaning |
|-----------|--------|
| `*/30 * * * *` | Every 30 minutes |
| `0 * * * *` | Every hour (at minute 0) |
| `0 */4 * * *` | Every 4 hours |
| `0 0 * * *` | Daily at midnight |
| `0 9 * * 1-5` | Weekdays at 9am |

Standard 5-field cron: minute hour day-of-month month day-of-week.

## Pattern 12: Setup and After Hooks (self-contained)

**Use when**: You need steps that run before the main workflow (setup)
or after it regardless of success/failure (after, for cleanup).

```yaml
apiVersion: testworkflows.testkube.io/v1
kind: TestWorkflow
metadata:
  name: setup-after-hooks
spec:
  container:
    image: alpine:latest
    resources:
      requests:
        cpu: 128m
        memory: 128Mi
  setup:
  - name: Prepare environment
    shell: echo "Setting up..." && mkdir -p /data/work
  steps:
  - name: Main task
    shell: echo "Doing work..." > /data/work/output.txt
  after:
  - name: Cleanup
    condition: always
    shell: echo "Cleaning up..." && rm -rf /data/work
```

`setup` steps run before `steps`. `after` steps run after everything, even if
previous steps failed. Use `condition: always` in after steps to ensure cleanup
executes regardless of failure. Both `setup` and `after` follow the same step
schema as `steps`.

## Pattern 13: Step Data Sharing

**Use when**: Passing values or files from one step to a later step within
the same pod. Two mechanisms: output values (strings) and results directories
(files).

### Output values

Write to `/testkube/outputs/<key>` in one step, read in another with
`{{ step.<id>.outputs.<key> }}`. Give steps an explicit `id` for readability.

```yaml
steps:
- name: Setup
  id: setup
  shell: |
    echo "my-generated-token" > /testkube/outputs/token
- name: Test
  id: test
  shell: |
    echo "Using token: {{ step.setup.outputs.token }}"
```

### Results directories

Each step has a results directory at `{{ step.results }}`. Files written there
are accessible to later steps at `{{ step.<id>.results }}/`. Use this for
binaries, config files, or test fixtures.

```yaml
steps:
- name: Build
  id: build
  shell: |
    echo 'echo "running"' > {{ step.results }}/app
    chmod +x {{ step.results }}/app
- name: Run
  id: run
  shell: |
    {{ step.build.results }}/app
```

Output values are strings — write JSON if you need structured data. Both
mechanisms work within a single pod. For cross-pod sharing (parallel workers),
use `transfer`/`fetch` (see Pattern 8).

## Pattern 14: Retry and Negative Testing

**Use when**: Handling flaky steps or testing error conditions.

### Retry with expressions

Use `retry` with an `until` expression for flaky operations:

```yaml
steps:
- name: Wait for service
  retry:
    count: 10
    until: self.passed
  shell: curl -f https://my-service/health
```

### Negative testing

Use `negative: true` to invert pass/fail — the step succeeds when the command
fails. Useful for testing error handling:

```yaml
steps:
- name: Should return 404
  negative: true
  shell: curl -f https://api.example.com/nonexistent
```

SHA-256: 53b057f3922f5a2b9f3297d340e9179c6b3af1e94ea5ff01dd5370fd98c9c3ac