# How to test Kyverno policies in CI with the Kyverno CLI

> Write kyverno-test.yaml files for a CEL ValidatingPolicy, run them with kyverno test, and make a CI job fail when a policy change breaks expected results.

Published: 2026-11-05
Author: k8wiz Team
URL: https://k8wiz.com/articles/test-kyverno-policies-in-ci-with-the-kyverno-cli

A Kyverno policy is code that runs in your admission path. A typo in an expression can block every Deployment in the cluster, or quietly let through the thing it was written to stop. Yet many teams still test policies by applying them to a staging cluster and watching what happens.

The Kyverno CLI can do better without any cluster. `kyverno test` applies policies to sample resources on your machine and compares the outcome with results you declare up front. This guide sets up tests for one CEL-based `ValidatingPolicy`, runs them, and wires them into CI. Field names and flags come from the Kyverno 1.19 CLI.

## Step 1: install the CLI

The CLI is a single binary called `kyverno`. Each Kyverno release on GitHub publishes it as an archive per platform. Pin the same version your cluster runs, so tests see the same policy engine as admission does.

```bash
VERSION=v1.19.2
curl -sSLO "https://github.com/kyverno/kyverno/releases/download/${VERSION}/kyverno-cli_${VERSION}_linux_x86_64.tar.gz"
tar -xzf "kyverno-cli_${VERSION}_linux_x86_64.tar.gz" kyverno
sudo install kyverno /usr/local/bin/kyverno
kyverno version
```

On macOS the archive name ends in `darwin_arm64` or `darwin_x86_64` instead.

## Step 2: lay out one directory per policy

`kyverno test` takes a directory and walks it recursively, looking for files named `kyverno-test.yaml`. Each test file points at policies and resources by path, relative to its own directory. Keeping each policy next to its test cases makes the paths short and the reviews easy.

```text
policies/
  require-team-label/
    policy.yaml
    resources.yaml
    kyverno-test.yaml
  disallow-latest-tag/
    policy.yaml
    resources.yaml
    kyverno-test.yaml
```

## Step 3: write the policy

The example policy requires a `team` label on every Deployment. It uses `ValidatingPolicy`, the CEL-based policy type Kyverno introduced alongside the older `ClusterPolicy`. The policy denies requests that fail the check.

```yaml
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
  name: require-team-label
spec:
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments"]
  validations:
    - expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
      message: "Deployments must have a team label."
```

The `has()` check matters. A Deployment without any labels has no `metadata.labels` map, and the expression would fail with an error instead of returning false. Testing exactly that case is one of the reasons to have tests.

## Step 4: write the sample resources

Put one resource per case you care about in `resources.yaml`. Give each a name that says what it tests, because the test file refers to resources by name and the output prints them.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: labeled
  namespace: shop
  labels:
    team: payments
spec:
  replicas: 1
  selector:
    matchLabels:
      app: labeled
  template:
    metadata:
      labels:
        app: labeled
    spec:
      containers:
        - name: app
          image: nginx:1.27
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: no-labels
  namespace: shop
spec:
  replicas: 1
  selector:
    matchLabels:
      app: no-labels
  template:
    metadata:
      labels:
        app: no-labels
    spec:
      containers:
        - name: app
          image: nginx:1.27
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wrong-label
  namespace: shop
  labels:
    owner: payments
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wrong-label
  template:
    metadata:
      labels:
        app: wrong-label
    spec:
      containers:
        - name: app
          image: nginx:1.27
```

Three cases cover the policy. One passes, one has no labels at all and one has labels but not the right one. When the policy changes later, add a resource for the new edge case before you change the expression.

## Step 5: declare the expected results

The test file is a Kubernetes-style manifest with its own API version. It lists the policies, the resources and what should happen to each resource.

```yaml
apiVersion: cli.kyverno.io/v1alpha1
kind: Test
metadata:
  name: require-team-label
policies:
  - policy.yaml
resources:
  - resources.yaml
results:
  - policy: require-team-label
    isValidatingPolicy: true
    kind: Deployment
    resources:
      - labeled
    result: pass
  - policy: require-team-label
    isValidatingPolicy: true
    kind: Deployment
    resources:
      - no-labels
      - wrong-label
    result: fail
```

Each entry under `results` names a policy, the kind and names of the resources it covers, and the expected `result`. For a policy that should block a resource, the expected result is `fail`. The other values you'll meet are `pass`, `skip`, `warn` and `error`.

`isValidatingPolicy: true` tells the CLI that the policy is a `ValidatingPolicy`. There are matching flags for the other new types, such as `isMutatingPolicy` and `isImageValidatingPolicy`. For a classic `ClusterPolicy`, leave the flag out and add `rule:` with the rule name instead, since a ClusterPolicy can hold several rules.

Write `apiVersion`, `kind` and `metadata.name` exactly as shown. The CLI rejects test files without `apiVersion` and `kind`, or with the old top-level `name` field, with an error that the file uses a deprecated schema. Older examples on the internet still use that format.

## Step 6: run the tests

Run the command from the repository root and point it at the policies directory.

```bash
kyverno test policies/
```

For each test file, the CLI prints which file it loaded, applies the policies and prints a table with the policy, the resource, the result and a reason. It ends with a summary line.

```text
Test Summary: 3 tests passed and 0 tests failed
```

To see the failure path, change the expected result for `labeled` to `fail` and run again. The table marks that row as failed, the summary counts it, and the command exits with a non-zero status. CI relies on that exit status.

While you work on one policy, run only its tests with the selector flag. The default selector is `policy=*,rule=*,resource=*`.

```bash
kyverno test policies/ --test-case-selector "policy=require-team-label"
kyverno test policies/ --detailed-results
```

`--detailed-results` adds the policy's message to each row, which helps when a resource fails for a different reason than you expected.

## Step 7: check a single resource quickly

`kyverno test` is for cases you want to keep. When you just want to know what a policy thinks of a manifest, `kyverno apply` is quicker and needs no test file.

```bash
kyverno apply policies/require-team-label/policy.yaml --resource deploy/api.yaml
```

It prints the violations it finds. Once you know how the manifest should be treated, add it to `resources.yaml` with the expected result, so the behavior is locked in.

## Step 8: run the tests in CI

The CI job needs the CLI and one command. Here is a GitLab CI job that installs the pinned version and runs every test.

```yaml
kyverno-tests:
  stage: test
  image: debian:bookworm-slim
  variables:
    KYVERNO_VERSION: v1.19.2
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - curl -sSLO "https://github.com/kyverno/kyverno/releases/download/${KYVERNO_VERSION}/kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz"
    - tar -xzf "kyverno-cli_${KYVERNO_VERSION}_linux_x86_64.tar.gz" kyverno
    - install kyverno /usr/local/bin/kyverno
  script:
    - kyverno test policies/ --require-tests --remove-color
  rules:
    - changes:
        - policies/**/*
```

`--require-tests` closes a quiet gap. Without it, a run that finds no `kyverno-test.yaml` files prints "No test yamls available" and still succeeds. A renamed directory or a wrong path would then turn the whole job into a no-op that always passes. With the flag, finding no tests is an error.

`--remove-color` drops the terminal color codes, which keeps CI logs readable. The CLI also offers `--warnings-as-errors`, which fails the run on deprecation warnings, so policies don't drift onto fields Kyverno plans to remove.

## What to test

Aim for at least one passing and one failing resource per validation, plus the edge cases that broke things before. Resources with no labels, empty lists and missing optional fields catch most expression bugs, because CEL raises an error when you read a field that isn't there.

If you use policy exceptions, test them too. A test file can list exception manifests under `exceptions:`, next to `policies:` and `resources:`, and the expected result for the exempted resource then becomes `skip`. That proves the exception matches only what it should.

Treat the test files as part of the policy. A pull request that changes a policy without changing or adding a test case deserves a question in review, the same as application code without tests.
