Helm chart testing: lint, unit tests, and ct in CI

Published: 2026-03-26

Untested Helm charts break in production in ways that are hard to debug: a missing required field, a label mismatch between Service and Deployment, a value that renders as null because of a typo in the values file. Three tools cover most of this: helm lint, helm-unittest, and chart-testing.


helm lint

The built-in linter catches syntax errors and required field violations:

bashhelm lint charts/my-service
helm lint charts/my-service -f charts/my-service/values-dev.yaml

Always lint with each environment values file, not just defaults. A value optional in values.yaml might be required in values-dev.yaml.

CI:

yamllint:
  stage: test
  image: alpine/helm:3.16
  script:
    - |
      for chart in charts/*/; do
        helm lint "$chart"
        helm lint "$chart" -f "${chart}values-dev.yaml" || true
      done

helm-unittest

helm-unittest renders templates with given values and asserts on the output. It catches logic errors that lint doesn't — wrong replicas, missing labels, incorrect resource names.

Install:

bashhelm plugin install https://github.com/helm-unittest/helm-unittest

Write tests in charts/my-service/tests/:

yaml# charts/my-service/tests/deployment_test.yaml
suite: deployment
templates:
  - deployment.yaml
tests:
  - it: should have 3 replicas in production
    asserts:
      - equal:
          path: spec.replicas
          value: 3

  - it: should have 1 replica in dev
    set:
      replicaCount: 1
    asserts:
      - equal:
          path: spec.replicas
          value: 1

  - it: should not allow privilege escalation
    asserts:
      - equal:
          path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
          value: false

  - it: should have CPU resource requests
    asserts:
      - isNotEmpty:
          path: spec.template.spec.containers[0].resources.requests.cpu

  - it: should set correct service selector
    templates:
      - service.yaml
    asserts:
      - equal:
          path: spec.selector.app
          value: my-service

Run:

bashhelm unittest charts/my-service

chart-testing (ct)

ct automates lint and install testing for changed charts in a monorepo. Detects changed charts, lints them, and optionally installs in a real cluster:

yaml# ct.yaml at repo root
target-branch: main
chart-dirs:
  - charts
check-version-increment: true
bash# Lint only changed charts
ct lint --config ct.yaml

# Install changed charts (needs a running cluster)
ct install --config ct.yaml

check-version-increment: true fails if you change chart templates without bumping Chart.yaml version. Enforces versioning discipline.


Stub values for ct install

ct install uses values from ci/ subdirectory inside the chart:

charts/my-service/
  ci/
    default-values.yaml    # minimal values for CI install
    dev-values.yaml        # dev environment values

Multiple files in ci/ run multiple installs — one per file. Tests the chart with different value combinations.


Full GitLab CI pipeline

yamlstages:
  - lint
  - test
  - package

helm-lint:
  stage: lint
  image: alpine/helm:3.16
  script:
    - helm lint charts/my-service

helm-unittest:
  stage: test
  image: alpine/helm:3.16
  before_script:
    - helm plugin install https://github.com/helm-unittest/helm-unittest
  script:
    - helm unittest charts/my-service

ct-lint:
  stage: lint
  image: quay.io/helmpack/chart-testing:latest
  script:
    - ct lint --config ct.yaml --charts charts/my-service

helm-package:
  stage: package
  image: alpine/helm:3.16
  script:
    - helm package charts/my-service --destination dist/
  artifacts:
    paths:
      - dist/*.tgz
  only:
    - main

What to test and what not to

Test:

  • Replicas set correctly per environment
  • Required secrets/configmaps referenced
  • SecurityContext fields (allowPrivilegeEscalation, readOnlyRootFilesystem)
  • Service selector matches Deployment labels
  • Resource requests/limits defined

Skip:

  • Kubernetes internals (that's k8s's job)
  • Runtime behavior (use integration tests)
  • Values that are trivially passed through

Common issues

helm-unittest plugin install in CI takes too long: Pre-bake the plugin into a custom image with alpine/helm:3.16 as base and RUN helm plugin install ....

ct lint fails on unchanged charts: Use --charts flag to target specific charts rather than scanning the whole repo for changes.